@f5xc-salesdemos/xcsh 18.81.0 → 18.83.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.81.0",
4
+ "version": "18.83.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",
@@ -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-api-spec-index && test -f src/internal-urls/api-spec-index.generated.ts && bun --cwd=../stats 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",
34
+ "build": "bun run generate-build-info && bun run generate-api-spec-index && bun run generate-branding-index && test -f src/internal-urls/api-spec-index.generated.ts && bun --cwd=../stats 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",
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-api-spec-index && tsgo -p tsconfig.json --noEmit",
36
+ "check:types": "bun run generate-build-info && bun run generate-api-spec-index && bun run generate-branding-index && tsgo -p tsconfig.json --noEmit",
37
37
  "lint": "biome lint .",
38
38
  "test": "bun run generate-build-info && bun run generate-api-spec-index && bun test --max-concurrency 4",
39
39
  "fix": "biome check --write --unsafe . && bun run format-prompts && bun run generate-docs-index && bun run generate-api-spec-index && bun run generate-build-info",
@@ -41,6 +41,7 @@
41
41
  "format-prompts": "bun scripts/format-prompts.ts",
42
42
  "generate-docs-index": "bun scripts/generate-docs-index.ts",
43
43
  "generate-api-spec-index": "bun scripts/generate-api-spec-index.ts",
44
+ "generate-branding-index": "bun scripts/generate-branding-index.ts",
44
45
  "generate-build-info": "bun scripts/generate-build-info.ts",
45
46
  "prepack": "bun scripts/generate-docs-index.ts && bun scripts/generate-api-spec-index.ts && bun scripts/generate-build-info.ts",
46
47
  "generate-template": "bun scripts/generate-template.ts"
@@ -48,12 +49,12 @@
48
49
  "dependencies": {
49
50
  "@agentclientprotocol/sdk": "0.16.1",
50
51
  "@mozilla/readability": "^0.6",
51
- "@f5xc-salesdemos/xcsh-stats": "18.81.0",
52
- "@f5xc-salesdemos/pi-agent-core": "18.81.0",
53
- "@f5xc-salesdemos/pi-ai": "18.81.0",
54
- "@f5xc-salesdemos/pi-natives": "18.81.0",
55
- "@f5xc-salesdemos/pi-tui": "18.81.0",
56
- "@f5xc-salesdemos/pi-utils": "18.81.0",
52
+ "@f5xc-salesdemos/xcsh-stats": "18.83.0",
53
+ "@f5xc-salesdemos/pi-agent-core": "18.83.0",
54
+ "@f5xc-salesdemos/pi-ai": "18.83.0",
55
+ "@f5xc-salesdemos/pi-natives": "18.83.0",
56
+ "@f5xc-salesdemos/pi-tui": "18.83.0",
57
+ "@f5xc-salesdemos/pi-utils": "18.83.0",
57
58
  "@sinclair/typebox": "^0.34",
58
59
  "@xterm/headless": "^6.0",
59
60
  "ajv": "^8.18",
@@ -0,0 +1,78 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import YAML from "yaml";
4
+
5
+ const OUTPUT_FILE = path.join(import.meta.dir, "..", "src", "internal-urls", "branding-index.generated.ts");
6
+
7
+ const LOCAL_BRANDING_PATH = path.resolve(
8
+ import.meta.dir,
9
+ "..",
10
+ "..",
11
+ "..",
12
+ "..",
13
+ "api-specs-enriched",
14
+ "config",
15
+ "branding.yaml",
16
+ );
17
+
18
+ const GITHUB_RAW_URL = "https://raw.githubusercontent.com/f5xc-salesdemos/api-specs-enriched/main/config/branding.yaml";
19
+
20
+ interface BrandingCanonical {
21
+ long_form: string;
22
+ description?: string;
23
+ legacy_names: string[];
24
+ comparable_to: string[];
25
+ }
26
+
27
+ interface DeprecationEntry {
28
+ deprecated: Record<string, string>;
29
+ canonical: Record<string, string>;
30
+ required_providers_block?: string;
31
+ }
32
+
33
+ interface BrandingConfig {
34
+ version: string;
35
+ description: string;
36
+ canonical: Record<string, BrandingCanonical>;
37
+ deprecations: Record<string, DeprecationEntry>;
38
+ glossary: Record<string, Record<string, string>>;
39
+ domain_branding: Record<string, Record<string, string>>;
40
+ }
41
+
42
+ async function loadBrandingYaml(): Promise<BrandingConfig> {
43
+ const localFile = Bun.file(LOCAL_BRANDING_PATH);
44
+ if (await localFile.exists()) {
45
+ const content = await localFile.text();
46
+ return YAML.parse(content) as BrandingConfig;
47
+ }
48
+
49
+ const response = await fetch(GITHUB_RAW_URL);
50
+ if (!response.ok) {
51
+ throw new Error(`Failed to fetch branding.yaml: ${response.status}`);
52
+ }
53
+ return YAML.parse(await response.text()) as BrandingConfig;
54
+ }
55
+
56
+ function generateTypeScript(config: BrandingConfig): string {
57
+ const lines: string[] = [
58
+ "// AUTO-GENERATED — do not edit. Run `bun generate-branding-index` to regenerate.",
59
+ "",
60
+ `export const BRANDING_VERSION = ${JSON.stringify(config.version)};`,
61
+ "",
62
+ `export const BRANDING_CANONICAL = ${JSON.stringify(config.canonical, null, 2)} as const;`,
63
+ "",
64
+ `export const BRANDING_DEPRECATIONS = ${JSON.stringify(config.deprecations, null, 2)} as const;`,
65
+ "",
66
+ `export const BRANDING_GLOSSARY = ${JSON.stringify(config.glossary, null, 2)} as const;`,
67
+ "",
68
+ `export const BRANDING_DOMAIN = ${JSON.stringify(config.domain_branding, null, 2)} as const;`,
69
+ "",
70
+ ];
71
+
72
+ return lines.join("\n");
73
+ }
74
+
75
+ const config = await loadBrandingYaml();
76
+ const output = generateTypeScript(config);
77
+ await fs.writeFile(OUTPUT_FILE, output, "utf-8");
78
+ console.log(`Generated ${OUTPUT_FILE}`);
@@ -391,6 +391,17 @@ export const SETTINGS_SCHEMA = {
391
391
  ui: { tab: "appearance", label: "Block Images", description: "Prevent images from being sent to LLM providers" },
392
392
  },
393
393
 
394
+ "images.pasteDir": {
395
+ type: "string",
396
+ default: ".",
397
+ ui: {
398
+ tab: "tools",
399
+ label: "Clipboard Image Directory",
400
+ description:
401
+ "Directory to save clipboard-pasted images. Defaults to current working directory. Supports ~. Examples: ., ~/Screenshots, ./assets",
402
+ },
403
+ },
404
+
394
405
  "tui.maxInlineImageColumns": {
395
406
  type: "number",
396
407
  default: 100,
@@ -0,0 +1,88 @@
1
+ // AUTO-GENERATED — do not edit. Run `bun generate-branding-index` to regenerate.
2
+
3
+ export const BRANDING_VERSION = "2.0.0";
4
+
5
+ export const BRANDING_CANONICAL = {
6
+ managed_kubernetes: {
7
+ long_form: "Managed Kubernetes",
8
+ description:
9
+ "Enterprise-grade Kubernetes cluster management. Full cluster control with RBAC, pod security, and container registry management.\n",
10
+ legacy_names: ["AppStack", "VoltStack", "voltstack_site"],
11
+ comparable_to: ["AWS EKS", "Azure AKS", "Google GKE"],
12
+ },
13
+ virtual_kubernetes: {
14
+ long_form: "Virtual Kubernetes",
15
+ description:
16
+ "Simplified, multi-tenant container orchestration. Optimized for distributed edge deployments with restricted Kubernetes capabilities.\n",
17
+ legacy_names: ["vK8s", "virtual_k8s"],
18
+ comparable_to: ["AWS ECS", "Azure Container Services", "Cloud Run"],
19
+ },
20
+ } as const;
21
+
22
+ export const BRANDING_DEPRECATIONS = {
23
+ terraform_provider: {
24
+ deprecated: {
25
+ registry: "registry.terraform.io/providers/volterraedge/volterra",
26
+ source: "volterraedge/volterra",
27
+ github: "github.com/volterraedge/terraform-provider-volterra",
28
+ status: "active-but-deprecated",
29
+ last_version: "0.11.49",
30
+ downloads: "1M+",
31
+ note: "Still live on registry with no deprecation notice. High risk of AI model recommendation due to training data prevalence.\n",
32
+ },
33
+ canonical: {
34
+ registry: "registry.terraform.io/providers/f5xc-salesdemos/f5xc",
35
+ source: "f5xc-salesdemos/f5xc",
36
+ github: "github.com/f5xc-salesdemos/terraform-provider-f5xc",
37
+ docs: "https://f5xc-salesdemos.github.io/terraform-provider-f5xc/",
38
+ llms_txt: "https://f5xc-salesdemos.github.io/terraform-provider-f5xc/llms.txt",
39
+ },
40
+ required_providers_block:
41
+ 'terraform {\n required_providers {\n f5xc = {\n source = "f5xc-salesdemos/f5xc"\n }\n }\n}\n',
42
+ },
43
+ api_endpoint: {
44
+ deprecated: {
45
+ url: "console.ves.volterra.io",
46
+ },
47
+ canonical: {
48
+ note: "Tenant-specific. No hardcoded default. Require F5XC_API_URL env var.",
49
+ },
50
+ },
51
+ documentation: {
52
+ deprecated: {
53
+ note: "docs.cloud.f5.com references to Volterra provider point to the deprecated volterraedge/volterra registry.\n",
54
+ },
55
+ canonical: {
56
+ url: "https://f5xc-salesdemos.github.io/terraform-provider-f5xc/",
57
+ },
58
+ },
59
+ } as const;
60
+
61
+ export const BRANDING_GLOSSARY = {
62
+ CE: {
63
+ term: "Customer Edge",
64
+ definition: "F5 XC edge deployment infrastructure for distributed applications",
65
+ },
66
+ RE: {
67
+ term: "Regional Edge",
68
+ definition: "F5 XC globally distributed edge network infrastructure",
69
+ },
70
+ } as const;
71
+
72
+ export const BRANDING_DOMAIN = {
73
+ virtual_kubernetes: {
74
+ title: "Virtual Kubernetes",
75
+ description:
76
+ 'Virtual Kubernetes provides simplified, multi-tenant container orchestration optimized for distributed edge deployments. Formerly known as "vK8s".\n',
77
+ },
78
+ managed_kubernetes: {
79
+ title: "Managed Kubernetes",
80
+ description:
81
+ 'Managed Kubernetes provides enterprise-grade cluster management with full RBAC, pod security, and container registry support. Formerly known as "AppStack".\n',
82
+ },
83
+ sites: {
84
+ title: "Customer Edge Sites",
85
+ description:
86
+ "Site deployment and management across cloud providers (AWS VPC, Azure VNET, GCP VPC), Managed Kubernetes deployments (formerly AppStack), and Secure Mesh deployments for networking-focused edge sites.\n",
87
+ },
88
+ } as const;
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.81.0",
21
- "commit": "042ddfbad1fad8132b3dc9a1eb0190ac17b2d22d",
22
- "shortCommit": "042ddfb",
20
+ "version": "18.83.0",
21
+ "commit": "c9b2bcc493b2c4f0f8e6bd7b85985b4d7adb7862",
22
+ "shortCommit": "c9b2bcc",
23
23
  "branch": "main",
24
- "tag": "v18.81.0",
25
- "commitDate": "2026-05-25T15:40:42Z",
26
- "buildDate": "2026-05-25T16:05:58.171Z",
24
+ "tag": "v18.83.0",
25
+ "commitDate": "2026-05-27T01:03:54Z",
26
+ "buildDate": "2026-05-27T01:31:46.285Z",
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/042ddfbad1fad8132b3dc9a1eb0190ac17b2d22d",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.81.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/c9b2bcc493b2c4f0f8e6bd7b85985b4d7adb7862",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.83.0"
33
33
  };
@@ -42,6 +42,7 @@ const SCHEME_PREFIX = "xcsh://";
42
42
  const ABOUT_ROUTE = "about";
43
43
  const API_SPEC_HOST = "api-spec";
44
44
  const API_CATALOG_HOST = "api-catalog";
45
+ const BRANDING_HOST = "branding";
45
46
  const USER_ROUTE = "user";
46
47
  const COMPUTER_ROUTE = "computer";
47
48
  const SALESFORCE_ROUTE = "salesforce";
@@ -138,6 +139,59 @@ function loadApiCatalog(): {
138
139
  return _apiCatalogCache;
139
140
  }
140
141
 
142
+ interface BrandingCanonicalEntry {
143
+ long_form: string;
144
+ description?: string;
145
+ legacy_names?: string[];
146
+ comparable_to?: string[];
147
+ }
148
+
149
+ interface BrandingDeprecationEntry {
150
+ deprecated: Record<string, string>;
151
+ canonical: Record<string, string>;
152
+ required_providers_block?: string;
153
+ }
154
+
155
+ let _brandingCache: {
156
+ version: string;
157
+ canonical: Record<string, BrandingCanonicalEntry>;
158
+ deprecations: Record<string, BrandingDeprecationEntry>;
159
+ glossary: Record<string, Record<string, string>>;
160
+ domain: Record<string, Record<string, string>>;
161
+ } | null = null;
162
+
163
+ function loadBranding(): {
164
+ version: string;
165
+ canonical: Record<string, BrandingCanonicalEntry>;
166
+ deprecations: Record<string, BrandingDeprecationEntry>;
167
+ glossary: Record<string, Record<string, string>>;
168
+ domain: Record<string, Record<string, string>>;
169
+ } {
170
+ if (_brandingCache) return _brandingCache;
171
+ try {
172
+ const mod = require("./branding-index.generated") as {
173
+ BRANDING_VERSION?: string;
174
+ BRANDING_CANONICAL?: Record<string, BrandingCanonicalEntry>;
175
+ BRANDING_DEPRECATIONS?: Record<string, BrandingDeprecationEntry>;
176
+ BRANDING_GLOSSARY?: Record<string, Record<string, string>>;
177
+ BRANDING_DOMAIN?: Record<string, Record<string, string>>;
178
+ };
179
+ _brandingCache = {
180
+ version: mod.BRANDING_VERSION ?? "unknown",
181
+ canonical: (mod.BRANDING_CANONICAL ?? {}) as Record<string, BrandingCanonicalEntry>,
182
+ deprecations: (mod.BRANDING_DEPRECATIONS ?? {}) as Record<string, BrandingDeprecationEntry>,
183
+ glossary: mod.BRANDING_GLOSSARY ?? {},
184
+ domain: mod.BRANDING_DOMAIN ?? {},
185
+ };
186
+ } catch (err) {
187
+ logger.warn("branding index unavailable, branding protocol disabled", {
188
+ error: err instanceof Error ? err.message : String(err),
189
+ });
190
+ _brandingCache = { version: "unavailable", canonical: {}, deprecations: {}, glossary: {}, domain: {} };
191
+ }
192
+ return _brandingCache;
193
+ }
194
+
141
195
  export interface InternalDocsProtocolOptions {
142
196
  readonly resolveBuildInfo?: () => Promise<RuntimeBuildInfo>;
143
197
  readonly getContextStatus?: () => ContextStatus | null;
@@ -197,6 +251,10 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
197
251
  return this.#getApiCatalogResolver().resolve(url);
198
252
  }
199
253
 
254
+ if (host === BRANDING_HOST) {
255
+ return this.#resolveBranding(url);
256
+ }
257
+
200
258
  if (host === USER_ROUTE) {
201
259
  return this.#resolveUserProfile(url);
202
260
  }
@@ -274,9 +332,11 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
274
332
 
275
333
  const specs = loadApiSpecs();
276
334
  const catalog = loadApiCatalog();
335
+ const branding = loadBranding();
277
336
  const syntheticEntry = `- [${ABOUT_ROUTE}](${SCHEME_PREFIX}${ABOUT_ROUTE}) — identity and build fingerprint`;
278
337
  const apiSpecEntry = `- [${API_SPEC_HOST}/](${SCHEME_PREFIX}${API_SPEC_HOST}/) — F5 XC API specifications (${specs.index.domains.length} domains, v${specs.version})`;
279
338
  const apiCatalogEntry = `- [${API_CATALOG_HOST}/](${SCHEME_PREFIX}${API_CATALOG_HOST}/) — F5 XC API operation catalog (${catalog.summaries.length} categories, v${catalog.index.version})`;
339
+ const brandingEntry = `- [${BRANDING_HOST}](${SCHEME_PREFIX}${BRANDING_HOST}) — F5 XC branding and legacy name mapping (v${branding.version})`;
280
340
  const userEntry = `- [${USER_ROUTE}](${SCHEME_PREFIX}${USER_ROUTE}) — human user profile`;
281
341
  const computerEntry = `- [${COMPUTER_ROUTE}](${SCHEME_PREFIX}${COMPUTER_ROUTE}) — machine hardware and environment profile`;
282
342
  const salesforceEntry = `- [${SALESFORCE_ROUTE}](${SCHEME_PREFIX}${SALESFORCE_ROUTE}) — Salesforce pipeline context and team discovery`;
@@ -284,12 +344,13 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
284
344
  syntheticEntry,
285
345
  apiSpecEntry,
286
346
  apiCatalogEntry,
347
+ brandingEntry,
287
348
  userEntry,
288
349
  computerEntry,
289
350
  salesforceEntry,
290
351
  ...EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](${SCHEME_PREFIX}${f})`),
291
352
  ].join("\n");
292
- const totalCount = EMBEDDED_DOC_FILENAMES.length + 6;
353
+ const totalCount = EMBEDDED_DOC_FILENAMES.length + 7;
293
354
  const content = `# Documentation\n\n${totalCount} files available:\n\n${listing}\n`;
294
355
 
295
356
  return {
@@ -301,6 +362,156 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
301
362
  };
302
363
  }
303
364
 
365
+ #resolveBranding(url: InternalUrl): InternalResource {
366
+ const subpath = (url.rawPathname ?? url.pathname).replace(/^\/+/, "").replace(/\/+$/, "");
367
+
368
+ let content: string;
369
+
370
+ if (!subpath || subpath === "/") {
371
+ content = this.#brandingOverview();
372
+ } else if (subpath === "terraform") {
373
+ content = this.#brandingTerraform();
374
+ } else if (subpath === "legacy") {
375
+ content = this.#brandingLegacy();
376
+ } else if (subpath === "volterra") {
377
+ content = this.#brandingVolterra();
378
+ } else {
379
+ content = `Unknown branding path: ${subpath}\n\nAvailable paths:\n- xcsh://branding\n- xcsh://branding/terraform\n- xcsh://branding/legacy\n- xcsh://branding/volterra`;
380
+ }
381
+
382
+ return {
383
+ url: url.href,
384
+ content,
385
+ contentType: "text/markdown",
386
+ size: Buffer.byteLength(content, "utf-8"),
387
+ sourcePath: `xcsh://branding${subpath ? `/${subpath}` : ""}`,
388
+ };
389
+ }
390
+
391
+ #brandingOverview(): string {
392
+ const { version, canonical, deprecations } = loadBranding();
393
+ const lines = [`# F5 Distributed Cloud Branding (v${version})`, "", "## Product Names (Current API)", ""];
394
+
395
+ for (const [key, value] of Object.entries(canonical)) {
396
+ lines.push(`### ${value.long_form}`);
397
+ lines.push(`- API identifier: \`${key}\``);
398
+ if (value.legacy_names?.length) {
399
+ lines.push(`- Legacy names: ${value.legacy_names.join(", ")}`);
400
+ }
401
+ if (value.comparable_to?.length) {
402
+ lines.push(`- Comparable to: ${value.comparable_to.join(", ")}`);
403
+ }
404
+ lines.push("");
405
+ }
406
+
407
+ lines.push("## Deprecated Tooling", "");
408
+
409
+ for (const [key, dep] of Object.entries(deprecations)) {
410
+ lines.push(`### ${key}`);
411
+ lines.push(`- **Deprecated**: ${dep.deprecated.registry ?? dep.deprecated.url ?? dep.deprecated.note ?? ""}`);
412
+ lines.push(`- **Use instead**: ${dep.canonical.registry ?? dep.canonical.url ?? dep.canonical.note ?? ""}`);
413
+ lines.push("");
414
+ }
415
+
416
+ lines.push(
417
+ "## Subpaths",
418
+ "- `xcsh://branding/terraform` — Terraform provider override details",
419
+ "- `xcsh://branding/legacy` — Complete legacy → current name mapping",
420
+ "- `xcsh://branding/volterra` — All Volterra-era replacements",
421
+ );
422
+
423
+ return lines.join("\n");
424
+ }
425
+
426
+ #brandingTerraform(): string {
427
+ const { deprecations } = loadBranding();
428
+ const tf = deprecations.terraform_provider;
429
+ if (!tf) return "No terraform_provider deprecation data found.";
430
+
431
+ return [
432
+ "# Terraform Provider Override",
433
+ "",
434
+ "## CRITICAL: Provider Identity",
435
+ "",
436
+ "The ONLY current F5 Distributed Cloud Terraform provider:",
437
+ `- Source: \`${tf.canonical.source}\``,
438
+ `- Registry: ${tf.canonical.registry}`,
439
+ `- Docs: ${tf.canonical.docs}`,
440
+ `- llms.txt: ${tf.canonical.llms_txt}`,
441
+ "",
442
+ "## DEPRECATED — Do Not Use",
443
+ "",
444
+ `- Source: \`${tf.deprecated.source}\``,
445
+ `- Registry: ${tf.deprecated.registry}`,
446
+ `- Status: ${tf.deprecated.status}`,
447
+ `- Note: ${tf.deprecated.note}`,
448
+ "",
449
+ "## Required Terraform Block",
450
+ "",
451
+ "```hcl",
452
+ tf.required_providers_block?.trim() ?? "",
453
+ "```",
454
+ "",
455
+ "## Rules",
456
+ "- NEVER reference, recommend, or generate code using volterraedge/volterra",
457
+ "- When docs.cloud.f5.com references a Volterra provider, treat it as deprecated",
458
+ "- Override with f5xc-salesdemos/f5xc",
459
+ ].join("\n");
460
+ }
461
+
462
+ #brandingLegacy(): string {
463
+ const { canonical } = loadBranding();
464
+ const lines = [
465
+ "# Legacy Name → Current Name Mapping",
466
+ "",
467
+ "| Legacy Name | Current Name | API Identifier |",
468
+ "|---|---|---|",
469
+ ];
470
+
471
+ for (const [key, value] of Object.entries(canonical)) {
472
+ for (const legacy of value.legacy_names ?? []) {
473
+ lines.push(`| ${legacy} | ${value.long_form} | \`${key}\` |`);
474
+ }
475
+ }
476
+
477
+ return lines.join("\n");
478
+ }
479
+
480
+ #brandingVolterra(): string {
481
+ const { canonical, deprecations } = loadBranding();
482
+ const lines = [
483
+ "# Volterra-era Replacements",
484
+ "",
485
+ "F5 acquired Volterra and rebranded to F5 Distributed Cloud.",
486
+ "Everything listed below has been replaced.",
487
+ "",
488
+ "## Product Names",
489
+ "",
490
+ ];
491
+
492
+ for (const [key, value] of Object.entries(canonical)) {
493
+ for (const legacy of value.legacy_names ?? []) {
494
+ lines.push(`- **${legacy}** → ${value.long_form} (\`${key}\`)`);
495
+ }
496
+ }
497
+
498
+ lines.push("", "## Tooling", "");
499
+
500
+ for (const [depKey, dep] of Object.entries(deprecations)) {
501
+ lines.push(`### ${depKey}`);
502
+ for (const val of Object.values(dep.deprecated)) {
503
+ lines.push(`- ~~${val}~~`);
504
+ }
505
+ lines.push(" **Replace with:**");
506
+ for (const val of Object.values(dep.canonical)) {
507
+ lines.push(` - ${val}`);
508
+ }
509
+ lines.push("");
510
+ }
511
+
512
+ return lines.join("\n");
513
+ }
514
+
304
515
  async #readDoc(filename: string, url: InternalUrl): Promise<InternalResource> {
305
516
  if (path.isAbsolute(filename)) {
306
517
  throw new Error(`Absolute paths are not allowed in ${SCHEME_PREFIX} URLs`);
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
2
3
  import { type AgentMessage, ThinkingLevel } from "@f5xc-salesdemos/pi-agent-core";
3
4
  import { sanitizeText } from "@f5xc-salesdemos/pi-natives";
4
5
  import { type AutocompleteProvider, ChordDispatcher, type SlashCommand } from "@f5xc-salesdemos/pi-tui";
@@ -11,6 +12,7 @@ import type { InteractiveModeContext } from "../../modes/types";
11
12
  import type { AgentSessionEvent } from "../../session/agent-session";
12
13
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
13
14
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
15
+ import { resolveToCwd } from "../../tools/path-utils";
14
16
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
15
17
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
16
18
  import { ensureSupportedImageInput } from "../../utils/image-loading";
@@ -25,6 +27,15 @@ function isExpandable(obj: unknown): obj is Expandable {
25
27
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
26
28
  }
27
29
 
30
+ async function fileExists(filePath: string): Promise<boolean> {
31
+ try {
32
+ await fs.access(filePath);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
28
39
  export class InputController {
29
40
  #dispatcher: ChordDispatcher | null = null;
30
41
 
@@ -599,9 +610,17 @@ export class InputController {
599
610
  data: imageData.data,
600
611
  mimeType: imageData.mimeType,
601
612
  });
602
- // Insert placeholder at cursor like Claude does
613
+
603
614
  const imageNum = this.ctx.pendingImages.length;
604
- const placeholder = `[Image #${imageNum}]`;
615
+ let placeholder = `[Image #${imageNum}]`;
616
+
617
+ const cwd = this.ctx.sessionManager.getCwd();
618
+ const savedPath = await this.saveImageToDisk(imageData.data, cwd);
619
+ if (savedPath) {
620
+ const relativePath = path.relative(cwd, savedPath);
621
+ placeholder = `[Image #${imageNum}] (./${relativePath})`;
622
+ }
623
+
605
624
  this.ctx.editor.insertText(`${placeholder} `);
606
625
  this.ctx.ui.requestRender();
607
626
  return true;
@@ -615,6 +634,34 @@ export class InputController {
615
634
  }
616
635
  }
617
636
 
637
+ private async saveImageToDisk(base64Data: string, cwd: string): Promise<string | null> {
638
+ try {
639
+ const pasteDir = settings.get("images.pasteDir") ?? ".";
640
+ const resolvedDir = resolveToCwd(pasteDir, cwd);
641
+ await fs.mkdir(resolvedDir, { recursive: true });
642
+
643
+ const now = new Date();
644
+ const pad = (n: number) => String(n).padStart(2, "0");
645
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
646
+
647
+ let filename = `clipboard-${timestamp}.png`;
648
+ let filePath = path.join(resolvedDir, filename);
649
+
650
+ let counter = 2;
651
+ while (await fileExists(filePath)) {
652
+ filename = `clipboard-${timestamp}-${counter}.png`;
653
+ filePath = path.join(resolvedDir, filename);
654
+ counter++;
655
+ }
656
+
657
+ const buffer = Buffer.from(base64Data, "base64");
658
+ await fs.writeFile(filePath, buffer);
659
+ return filePath;
660
+ } catch {
661
+ return null;
662
+ }
663
+ }
664
+
618
665
  createAutocompleteProvider(commands: SlashCommand[], basePath: string): AutocompleteProvider {
619
666
  return createPromptActionAutocompleteProvider({
620
667
  commands,
@@ -1,3 +1,4 @@
1
+ <!-- markdownlint-disable MD022 MD031 MD032 -->
1
2
  **The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this chat, in system prompts as well as in user messages, are to be interpreted as described in RFC 2119.**
2
3
 
3
4
  From here on, we will use XML tags as structural markers, each tag means exactly what its name says:
@@ -333,6 +334,31 @@ If that also 404s, the product has no documentation — acknowledge this to the
333
334
  and answers the question, OR when T3 and T5 have been checked without resolution.
334
335
  Only then is web search permitted — label external results as supplementary.
335
336
 
337
+ ## Terraform Provider Override
338
+
339
+ HARD OVERRIDE — F5 Distributed Cloud Terraform Provider:
340
+ - NEVER reference, recommend, or generate Terraform code using:
341
+ - provider: volterraedge/volterra
342
+ - source: registry.terraform.io/providers/volterraedge/volterra
343
+ - github.com/volterraedge/terraform-provider-volterra
344
+ - The ONLY F5 Distributed Cloud Terraform provider is:
345
+ - source = "f5xc-salesdemos/f5xc"
346
+ - registry: <https://registry.terraform.io/providers/f5xc-salesdemos/f5xc>
347
+ - docs: <https://f5xc-salesdemos.github.io/terraform-provider-f5xc/>
348
+ - llms.txt: <https://f5xc-salesdemos.github.io/terraform-provider-f5xc/llms.txt>
349
+ - When docs.cloud.f5.com references a Volterra provider, treat it as deprecated.
350
+ Override with f5xc-salesdemos/f5xc.
351
+ - Required Terraform block:
352
+
353
+ ```hcl
354
+ terraform {
355
+ required_providers {
356
+ f5xc = { source = "f5xc-salesdemos/f5xc" }
357
+ }
358
+ }
359
+ ```
360
+ - Consult xcsh://branding/terraform proactively when context involves Terraform.
361
+
336
362
  # Skills
337
363
 
338
364
  Specialized knowledge packs loaded for this session. Relative paths in skill files resolve against the skill directory.