@f5-sales-demo/xcsh 19.74.2 → 19.75.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.74.2",
4
+ "version": "19.75.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",
@@ -56,13 +56,13 @@
56
56
  "dependencies": {
57
57
  "@agentclientprotocol/sdk": "0.16.1",
58
58
  "@mozilla/readability": "^0.6",
59
- "@f5-sales-demo/xcsh-stats": "19.74.2",
60
- "@f5-sales-demo/pi-agent-core": "19.74.2",
61
- "@f5-sales-demo/pi-ai": "19.74.2",
62
- "@f5-sales-demo/pi-natives": "19.74.2",
63
- "@f5-sales-demo/pi-resource-management": "19.74.2",
64
- "@f5-sales-demo/pi-tui": "19.74.2",
65
- "@f5-sales-demo/pi-utils": "19.74.2",
59
+ "@f5-sales-demo/xcsh-stats": "19.75.0",
60
+ "@f5-sales-demo/pi-agent-core": "19.75.0",
61
+ "@f5-sales-demo/pi-ai": "19.75.0",
62
+ "@f5-sales-demo/pi-natives": "19.75.0",
63
+ "@f5-sales-demo/pi-resource-management": "19.75.0",
64
+ "@f5-sales-demo/pi-tui": "19.75.0",
65
+ "@f5-sales-demo/pi-utils": "19.75.0",
66
66
  "@sinclair/typebox": "^0.34",
67
67
  "@xterm/headless": "^6.0",
68
68
  "ajv": "^8.20",
@@ -26,7 +26,7 @@ export interface ExtensionCapabilities {
26
26
 
27
27
  export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
28
28
  "version": "0.1.0",
29
- "contractVersion": "1.9.0",
29
+ "contractVersion": "1.10.0",
30
30
  "multiPortDiscovery": true,
31
31
  "protocol": "tool_request/result",
32
32
  "tools": [
@@ -855,6 +855,6 @@ export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
855
855
  }
856
856
  };
857
857
 
858
- export const EXTENSION_CONTRACT_VERSION = "1.9.0";
858
+ export const EXTENSION_CONTRACT_VERSION = "1.10.0";
859
859
 
860
860
  export const EXTENSION_TOOL_NAMES: readonly string[] = ["ping","capabilities","reload","debug_exec","detach","set_bridge_port","navigate","login","scroll_to","resize_window","tabs_list","tabs_create","tabs_close","click","click_element","click_xy","type_text","form_input","key_press","select_option","label_select","file_upload","read_ax","get_page_text","query_dom","find","wait_for","assert_text","screenshot","read_console","read_network","diag_suspension","diag_bridges","diag_activation","diag_ttft","capture_login_flow","wait_for_api_response","get_page_context","javascript_tool","browser_batch","set_explain_mode","annotate"];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.1.0",
3
- "contractVersion": "1.9.0",
3
+ "contractVersion": "1.10.0",
4
4
  "multiPortDiscovery": true,
5
5
  "protocol": "tool_request/result",
6
6
  "tools": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "contractVersion": "1.9.0",
2
+ "contractVersion": "1.10.0",
3
3
  "schemas": {
4
4
  "chat_request": {
5
5
  "type": "object",
@@ -32,6 +32,7 @@ import {
32
32
  } from "./chat-protocol";
33
33
  import { CONSOLE_ROUTES } from "./console-routes.generated";
34
34
  import type { BridgeServer } from "./extension-bridge";
35
+ import { type ClientHost, hostProfile } from "./host-profiles";
35
36
  import { interpretPageState } from "./page-state-interpreter";
36
37
  import { chatSpans } from "./ttft-spans";
37
38
 
@@ -163,7 +164,7 @@ export class ChatHandler {
163
164
  });
164
165
  chat.unsubscribe = unsubscribe;
165
166
 
166
- const prompt = composeChatPrompt(req.text, req.context, req.mode);
167
+ const prompt = composeChatPrompt(req.text, req.context, req.mode, this.#server.clientHost);
167
168
 
168
169
  try {
169
170
  chat.promptAt = Date.now();
@@ -435,49 +436,6 @@ export function classifyChatErrorReason(message: string): ChatErrorReason | unde
435
436
  return undefined;
436
437
  }
437
438
 
438
- /**
439
- * Chrome-extension self-awareness prompt. Injected when xcsh is serving a browser
440
- * chat (not the CLI TUI). Tells the LLM it's in a Chrome side panel alongside the
441
- * F5 XC console, what tools it has, and how to behave differently from the CLI.
442
- */
443
- const CHROME_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant for the F5 Distributed Cloud console, running as a Chrome browser side panel — not a terminal CLI.
444
-
445
- CRITICAL: ALWAYS respond with TEXT FIRST. Do NOT jump straight to tool calls. The user sees a chat panel and expects a conversational text response, not silence while tools run in the background. For questions ("what page am I on?", "what is this?"), answer with text using the page context below — no tools needed. Only use tools when the user explicitly asks you to DO something (create, navigate, click, modify).
446
-
447
- CONTEXT: The user sees a small chat window alongside the F5 XC admin console. You receive page-aware context each turn: the current URL (interpreted as workspace/resource/CRUD operation/namespace), the API resource JSON, and the accessibility tree. USE THIS CONTEXT to answer questions — don't call tools to find information you already have.
448
-
449
- BEHAVIOR:
450
- - Respond concisely with markdown. The chat panel is narrow — avoid long code blocks.
451
- - You KNOW which page the user is on (injected below). Don't ask "what page are you on?" — tell them.
452
- - For questions about the page/resource: answer from the injected context. No tools.
453
- - If a blocking popup/survey appears, dismiss it by clicking the close button.
454
- - If on the LOGIN page: use the login tool to log in. The login tool handles ALL environments — production (*.console.ves.volterra.io) AND staging (*.staging.volterra.us, login-staging.volterra.us). Do NOT claim the login tool is broken, unsupported, or doesn't work for staging — it does.
455
-
456
- BROWSER AUTOMATION (when the user asks to create/modify/navigate resources):
457
- - You are IN a Chrome browser. The active console tab is your workspace — use IT.
458
- - For create/modify/delete: call catalog_workflow_runner IMMEDIATELY with ONE tool call per resource:
459
- {"resource": "health-check", "operation": "create", "params": {"name": "foo", "namespace": "demo"}, "presentation": "guided"}
460
- Do NOT read API specs first, do NOT create todos, do NOT orchestrate multi-step tool chains. The catalog_workflow_runner handles ALL the form navigation internally.
461
- - Say a brief text message BEFORE the tool call: "Creating health check **foo** — watch the browser." Then call the tool. Nothing else.
462
- - The human is WATCHING the form automation (fingerprint-before-click, highlights, ~1.5s/step). Do NOT use background API calls.
463
- - The browser may be at 85% zoom — automation handles coordinates at any zoom.
464
- - The console catalog has workflows for 100+ F5 XC resources.
465
- - Do NOT open new tabs — drive the existing console tab.
466
-
467
- MULTI-RESOURCE REQUESTS (when the user asks to create several resources in one prompt):
468
- - Create resources in DEPENDENCY ORDER: health checks first, then origin pools (which reference health checks), then load balancers (which reference origin pools and app firewalls).
469
- - After each catalog_workflow_runner call completes, IMMEDIATELY proceed to the next resource. Do NOT inspect, verify, click into, or navigate to the resource you just created. Do NOT open the JSON view. Do NOT read the page to confirm — the tool already confirmed success. Move directly to the next creation.
470
- - Between resources, say ONE short line: "Health check created. Now creating origin pool **bar** — watch the browser." Then call the next tool.
471
- - NEVER navigate to a list/detail/JSON view between creations. Stay on the automation path.
472
-
473
- SAFETY — NEVER DO THESE:
474
- - NEVER kill, stop, or manage processes on port 19222 — that is YOUR OWN bridge. Killing it kills you.
475
- - NEVER run lsof, fuser, kill, or pkill on the bridge port. You ARE the bridge.
476
- - NEVER use bash/shell tools to manage xcsh processes, ports, or the debugger connection.
477
- - NEVER run commands that would terminate your own process or the WebSocket server.]
478
-
479
- `;
480
-
481
439
  const MODE_INSTRUCTIONS: Record<InteractionMode, string> = {
482
440
  educational: "Explain concepts and settings in depth. Help the user understand what they're looking at and why.",
483
441
  presentation: "Guide a structured walkthrough. Narrate each step clearly for a live audience.",
@@ -486,77 +444,94 @@ const MODE_INSTRUCTIONS: Record<InteractionMode, string> = {
486
444
  annotation: "Create on-page teaching annotations that highlight key elements and explain their purpose.",
487
445
  };
488
446
 
489
- export function composeChatPrompt(text: string, context: PageContextSnapshot | null, mode: InteractionMode): string {
447
+ export function composeChatPrompt(
448
+ text: string,
449
+ context: PageContextSnapshot | null,
450
+ mode: InteractionMode,
451
+ host: ClientHost | null,
452
+ ): string {
490
453
  const parts: string[] = [];
491
454
 
492
- // Chrome-extension self-awareness: establishes the agent's browser context.
493
- parts.push(CHROME_CHAT_SYSTEM_PROMPT);
494
- parts.push(`[Chat mode: ${mode}] ${MODE_INSTRUCTIONS[mode]}`);
455
+ // Host-aware self-awareness: the Chrome extension gets the browser panel prompt;
456
+ // an Office add-in (excel/powerpoint/word) gets its document-assistant prompt. A
457
+ // null/unannounced host falls back to the Chrome profile.
458
+ const profile = hostProfile(host);
459
+ parts.push(profile.systemPrompt);
460
+
461
+ // Browser hosts ALSO get an interaction mode + the page-context block. Document
462
+ // hosts get NEITHER: Office sends no page context and has no browser modes; its
463
+ // tools + document state arrive at runtime via set_host_tools.
464
+ if (profile.kind === "browser") {
465
+ parts.push(`[Chat mode: ${mode}] ${MODE_INSTRUCTIONS[mode]}`);
466
+ if (context) composeBrowserPageContext(parts, context);
467
+ }
495
468
 
496
- if (context) {
497
- // Interpret the raw URL into structured page state (workspace, resource,
498
- // CRUD operation, namespace) using deterministic route-pattern matching
499
- // against console_ui.yaml — the LLM sees "origin_pool LIST in demo" not a
500
- // raw URL it must guess about.
501
- const pageState = interpretPageState(context.url, null, CONSOLE_ROUTES);
469
+ parts.push("");
470
+ parts.push(text);
471
+ return parts.join("\n");
472
+ }
502
473
 
503
- parts.push("");
504
- parts.push(`[Page contextcaptured at ${new Date(context.capturedAt).toISOString()}]`);
474
+ /** Append the Chrome-only page-context block (interpreted page state + API body +
475
+ * accessibility tree) to `parts`. Browser hosts only Office sends no context. */
476
+ function composeBrowserPageContext(parts: string[], context: PageContextSnapshot): void {
477
+ // Interpret the raw URL into structured page state (workspace, resource,
478
+ // CRUD operation, namespace) using deterministic route-pattern matching
479
+ // against console_ui.yaml — the LLM sees "origin_pool LIST in demo" not a
480
+ // raw URL it must guess about.
481
+ const pageState = interpretPageState(context.url, null, CONSOLE_ROUTES);
505
482
 
506
- // Tenant + environment (the LLM knows WHICH tenant on WHICH environment).
507
- if (pageState.tenant || pageState.environment) {
508
- parts.push(`Tenant: ${pageState.tenant ?? "unknown"} (${pageState.environment ?? "unknown"} environment)`);
509
- }
483
+ parts.push("");
484
+ parts.push(`[Page context — captured at ${new Date(context.capturedAt).toISOString()}]`);
510
485
 
511
- // Structured page state (the interpreted context the LLM acts on).
512
- if (pageState.operation === "login") {
513
- parts.push("Page: LOGIN session expired or first login. The user is on the Keycloak authentication page.");
514
- parts.push(
515
- "Use the login tool with their email and password to log in. The login tool handles both production and staging Keycloak (including login-staging.volterra.us). Do NOT bypass it or claim it doesn't support staging.",
516
- );
517
- } else if (pageState.resource) {
518
- const opLabel = pageState.operation.toUpperCase();
519
- const nsLabel = pageState.namespace ? ` in namespace "${pageState.namespace}"` : "";
520
- const nameLabel = pageState.resourceName ? ` — instance "${pageState.resourceName}"` : "";
521
- parts.push(`Page: ${pageState.resource} ${opLabel}${nameLabel} (workspace: ${pageState.workspace}${nsLabel})`);
522
- } else {
523
- parts.push(`Page: ${context.title} (unrecognized resource)`);
524
- }
525
- if (pageState.modalBlocking) {
526
- parts.push(`⚠ Modal blocking: ${pageState.modalText ?? "unknown overlay"}`);
527
- }
528
- parts.push(`URL: ${context.url}`);
529
- parts.push(`Title: ${context.title}`);
530
-
531
- if (context.api) {
532
- parts.push(
533
- `API resource (${context.api.resourceType ?? "unknown"}, status ${context.api.status}): ${context.api.url}`,
534
- );
535
- if (context.api.body) {
536
- const body =
537
- typeof context.api.body === "string" ? context.api.body : JSON.stringify(context.api.body, null, 2);
538
- parts.push(body);
539
- }
540
- if (context.api.truncated) {
541
- parts.push("[API body was truncated]");
542
- }
543
- }
486
+ // Tenant + environment (the LLM knows WHICH tenant on WHICH environment).
487
+ if (pageState.tenant || pageState.environment) {
488
+ parts.push(`Tenant: ${pageState.tenant ?? "unknown"} (${pageState.environment ?? "unknown"} environment)`);
489
+ }
544
490
 
545
- if (context.ax) {
546
- const ax = typeof context.ax === "string" ? context.ax : JSON.stringify(context.ax);
547
- parts.push(`Accessibility tree: ${ax}`);
491
+ // Structured page state (the interpreted context the LLM acts on).
492
+ if (pageState.operation === "login") {
493
+ parts.push("Page: LOGIN — session expired or first login. The user is on the Keycloak authentication page.");
494
+ parts.push(
495
+ "Use the login tool with their email and password to log in. The login tool handles both production and staging Keycloak (including login-staging.volterra.us). Do NOT bypass it or claim it doesn't support staging.",
496
+ );
497
+ } else if (pageState.resource) {
498
+ const opLabel = pageState.operation.toUpperCase();
499
+ const nsLabel = pageState.namespace ? ` in namespace "${pageState.namespace}"` : "";
500
+ const nameLabel = pageState.resourceName ? ` — instance "${pageState.resourceName}"` : "";
501
+ parts.push(`Page: ${pageState.resource} ${opLabel}${nameLabel} (workspace: ${pageState.workspace}${nsLabel})`);
502
+ } else {
503
+ parts.push(`Page: ${context.title} (unrecognized resource)`);
504
+ }
505
+ if (pageState.modalBlocking) {
506
+ parts.push(`⚠ Modal blocking: ${pageState.modalText ?? "unknown overlay"}`);
507
+ }
508
+ parts.push(`URL: ${context.url}`);
509
+ parts.push(`Title: ${context.title}`);
510
+
511
+ if (context.api) {
512
+ parts.push(
513
+ `API resource (${context.api.resourceType ?? "unknown"}, status ${context.api.status}): ${context.api.url}`,
514
+ );
515
+ if (context.api.body) {
516
+ const body =
517
+ typeof context.api.body === "string" ? context.api.body : JSON.stringify(context.api.body, null, 2);
518
+ parts.push(body);
548
519
  }
549
-
550
- if (context.truncated) {
551
- parts.push("[Page context was truncated]");
520
+ if (context.api.truncated) {
521
+ parts.push("[API body was truncated]");
552
522
  }
523
+ }
553
524
 
554
- parts.push("---");
525
+ if (context.ax) {
526
+ const ax = typeof context.ax === "string" ? context.ax : JSON.stringify(context.ax);
527
+ parts.push(`Accessibility tree: ${ax}`);
555
528
  }
556
529
 
557
- parts.push("");
558
- parts.push(text);
559
- return parts.join("\n");
530
+ if (context.truncated) {
531
+ parts.push("[Page context was truncated]");
532
+ }
533
+
534
+ parts.push("---");
560
535
  }
561
536
 
562
537
  export function classifyReferenceKind(url: string): "doc" | "console" {
@@ -15,6 +15,11 @@ import type {
15
15
  RpcHostToolUpdate,
16
16
  } from "../host-tools/types";
17
17
 
18
+ // The client host announced on the `hello` handshake (contract 1.10.0). Re-exported
19
+ // (type-only, fully erased) so browser-safe consumers — the office-pane bundle —
20
+ // can share the wire vocabulary without importing the host-profiles prompt data.
21
+ export type { ClientHost } from "./host-profiles";
22
+
18
23
  // ---------------------------------------------------------------------------
19
24
  // Page context snapshot (auto-attached by extension to every chat_request)
20
25
  // ---------------------------------------------------------------------------
@@ -97,3 +97,19 @@ export const BROWSER_TOOL_NAMES: readonly string[] = [
97
97
  "annotate",
98
98
  "set_explain_mode",
99
99
  ];
100
+
101
+ /**
102
+ * Builtin agent tools scoped into the headless OFFICE bridge (`xcsh office serve`).
103
+ * The Office task pane drives a document (Excel/PowerPoint/Word), NOT a browser, so
104
+ * it must get NONE of the {@link BROWSER_TOOL_NAMES} — those would be hallucinated
105
+ * against a host with no browser to drive. The document's own tools arrive at
106
+ * runtime over the bridge via `set_host_tools`; this list is only the minimal
107
+ * general-purpose, host-neutral builtin toolset a document assistant can safely use.
108
+ *
109
+ * NOTE: an EMPTY list cannot express "no builtin tools" — createAgentSession/createTools
110
+ * treat `[]` as "unscoped" and hand back the FULL builtin registry (bash/edit/python/
111
+ * browser/…). So we pass an explicit minimal set instead. `calc` is the one builtin
112
+ * that is pure computation — no browser, no shell, no filesystem, no network — and is
113
+ * genuinely useful for spreadsheet/document math.
114
+ */
115
+ export const OFFICE_TOOL_NAMES: readonly string[] = ["calc"];
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { Server, ServerWebSocket } from "bun";
3
3
  import { LOCALIP_HOST } from "./bridge-cert";
4
+ import { type ClientHost, isClientHost } from "./host-profiles";
4
5
 
5
6
  export interface ToolResult {
6
7
  content: unknown;
@@ -177,6 +178,9 @@ export class BridgeServer {
177
178
  #onMessage: Array<(msg: Record<string, unknown>) => void> = [];
178
179
  /** Heartbeat interval that sends pings to keep the MV3 service worker alive (sweep + chat). */
179
180
  #heartbeat: ReturnType<typeof setInterval> | null = null;
181
+ /** The client host learned from the `hello` handshake (null until a client
182
+ * announces one; the Chrome extension omits it → stays null → chrome profile). */
183
+ #clientHost: ClientHost | null = null;
180
184
  /** Provider of this process's tenant identity, answering the `hello` handshake. */
181
185
  #sessionInfo:
182
186
  | (() => {
@@ -203,6 +207,12 @@ export class BridgeServer {
203
207
  return this.#clients.size > 0;
204
208
  }
205
209
 
210
+ /** The client host announced by the connected client's `hello` (null until a
211
+ * client announces one). Read by the ChatHandler to pick the host-aware prompt. */
212
+ get clientHost(): ClientHost | null {
213
+ return this.#clientHost;
214
+ }
215
+
206
216
  /** Number of connected extension clients (channels). */
207
217
  get connectedCount(): number {
208
218
  return this.#clients.size;
@@ -364,7 +374,7 @@ export class BridgeServer {
364
374
 
365
375
  #handleMessage(ws: ServerWebSocket<undefined>, message: string | Buffer): void {
366
376
  const text = typeof message === "string" ? message : message.toString("utf8");
367
- let msg: { type?: string; id?: string; content?: unknown; is_error?: boolean };
377
+ let msg: { type?: string; id?: string; content?: unknown; is_error?: boolean; host?: unknown };
368
378
  try {
369
379
  msg = JSON.parse(text);
370
380
  } catch {
@@ -379,6 +389,11 @@ export class BridgeServer {
379
389
  ws.send(JSON.stringify({ type: "pong" }));
380
390
  } else if (msg.type === "hello") {
381
391
  // Identity handshake: tell the extension which tenant this process serves.
392
+ // Record the announced client host (contract 1.10.0): Office sends its
393
+ // lowercased Office.context.host ("excel"|"powerpoint"|"word"); the Chrome
394
+ // extension omits it → clientHost stays null → the browser profile. Invalid
395
+ // values are ignored (null retained). Echoed back so the client can confirm.
396
+ if (isClientHost(msg.host)) this.#clientHost = msg.host;
382
397
  const info = this.#sessionInfo?.() ?? {
383
398
  tenant: null,
384
399
  env: null,
@@ -396,6 +411,7 @@ export class BridgeServer {
396
411
  env: info.env,
397
412
  apiUrl: info.apiUrl,
398
413
  contextBound: info.contextBound,
414
+ host: this.#clientHost,
399
415
  pid: process.pid,
400
416
  wssPort: this.wssPort,
401
417
  canConfigureProvider: true,
@@ -21,7 +21,7 @@ import { deriveTenantEnv } from "../services/xcsh-env";
21
21
  import { resolveBridgeTls } from "./bridge-cert";
22
22
  import { ChatHandler } from "./chat-handler";
23
23
  import { type BridgeServer, startBridgeServer } from "./extension-bridge";
24
- import { BROWSER_TOOL_NAMES, createExtensionBridgeTools, EXTENSION_AGENT_TOOL_NAMES } from "./extension-bridge-tools";
24
+ import { OFFICE_TOOL_NAMES } from "./extension-bridge-tools";
25
25
  import { setSharedBridgeServer } from "./provider";
26
26
 
27
27
  /** A running headless bridge + a teardown that disposes the chat handler and
@@ -67,7 +67,6 @@ export interface HeadlessBridgeDeps {
67
67
  resolveBridgeTls: typeof resolveBridgeTls;
68
68
  startBridgeServer: typeof startBridgeServer;
69
69
  setSharedBridgeServer: typeof setSharedBridgeServer;
70
- createExtensionBridgeTools: typeof createExtensionBridgeTools;
71
70
  createAgentSession: typeof createAgentSession;
72
71
  ChatHandlerCtor: typeof ChatHandler;
73
72
  }
@@ -95,7 +94,6 @@ const defaultDeps: HeadlessBridgeDeps = {
95
94
  resolveBridgeTls,
96
95
  startBridgeServer,
97
96
  setSharedBridgeServer,
98
- createExtensionBridgeTools,
99
97
  createAgentSession,
100
98
  ChatHandlerCtor: ChatHandler,
101
99
  };
@@ -130,15 +128,17 @@ export async function startHeadlessChatBridge(deps: HeadlessBridgeDeps = default
130
128
  // selectProvider() reuses a dead bridge. The caller (startOfficeServe) treats
131
129
  // the rethrow as a non-fatal "pane only" fallback.
132
130
  try {
133
- // Turn the extension's browser actions into bridge-proxying CustomTools, then
134
- // create ONE headless session scoped to the browser tools (Office document
135
- // tools arrive at runtime via set_host_tools).
136
- const extensionTools = deps.createExtensionBridgeTools(bridge);
131
+ // Create ONE headless Office session scoped to the minimal general builtin set
132
+ // (OFFICE_TOOL_NAMES) with NO browser tools neither the browser BUILTINS
133
+ // (navigate/click/…) nor the bridge-proxying browser CUSTOM tools, both of which
134
+ // would be hallucinated in a document task pane (there is no browser to drive).
135
+ // The document's own tools (Excel/Word/PowerPoint) arrive at runtime via
136
+ // set_host_tools; chat over xcsh's configured provider needs no browser tooling.
137
137
  const { session } = await deps.createAgentSession({
138
138
  cwd,
139
139
  hasUI: false,
140
- toolNames: [...new Set([...BROWSER_TOOL_NAMES, ...EXTENSION_AGENT_TOOL_NAMES])],
141
- customTools: extensionTools,
140
+ toolNames: [...OFFICE_TOOL_NAMES],
141
+ customTools: [],
142
142
  // Headless: no MCP/LSP/extension discovery — lean, no network/blocking prompts.
143
143
  enableMCP: false,
144
144
  enableLsp: false,
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Host-aware system prompts for the bridge chat engine.
3
+ *
4
+ * The same `hello`/`hello_ack` bridge serves two very different clients: the
5
+ * Chrome extension (a browser side panel driving the F5 XC console) and the
6
+ * Office add-in task pane (Excel / PowerPoint / Word, driving the open
7
+ * document). A `host` field on the handshake tells the engine which one it is,
8
+ * so `composeChatPrompt` can inject the RIGHT self-awareness prompt instead of
9
+ * unconditionally telling every client it is a Chrome browser panel.
10
+ *
11
+ * Browser-safe: pure string/data module, no node/bun imports — so it can be
12
+ * type-imported by the office-pane's browser bundle via the native contract.
13
+ */
14
+
15
+ /**
16
+ * The client hosts the bridge can serve, as lowercase wire values. Office maps
17
+ * its `Office.context.host` ("Excel"|"PowerPoint"|"Word") to these; the Chrome
18
+ * extension is "chrome". Outlook is intentionally absent — the add-in omits the
19
+ * host for hosts without a document assistant, so the engine falls back to the
20
+ * default profile.
21
+ */
22
+ export type ClientHost = "chrome" | "excel" | "powerpoint" | "word";
23
+
24
+ /** Every {@link ClientHost} value, for iteration + validation. */
25
+ export const CLIENT_HOSTS: readonly ClientHost[] = ["chrome", "excel", "powerpoint", "word"];
26
+
27
+ /** Runtime guard: true when `v` is a valid {@link ClientHost} wire value. */
28
+ export function isClientHost(v: unknown): v is ClientHost {
29
+ return typeof v === "string" && (CLIENT_HOSTS as readonly string[]).includes(v);
30
+ }
31
+
32
+ /**
33
+ * A host profile pairs a `kind` (browser vs document) with the self-awareness
34
+ * system prompt for that host. `kind` gates browser-only behavior in
35
+ * `composeChatPrompt` (interaction modes + the page-context block) — document
36
+ * hosts get neither.
37
+ */
38
+ export interface HostProfile {
39
+ kind: "browser" | "document";
40
+ systemPrompt: string;
41
+ }
42
+
43
+ /**
44
+ * Chrome-extension self-awareness prompt. Injected when xcsh is serving a browser
45
+ * chat (not the CLI TUI). Tells the LLM it's in a Chrome side panel alongside the
46
+ * F5 XC console, what tools it has, and how to behave differently from the CLI.
47
+ */
48
+ export const CHROME_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant for the F5 Distributed Cloud console, running as a Chrome browser side panel — not a terminal CLI.
49
+
50
+ CRITICAL: ALWAYS respond with TEXT FIRST. Do NOT jump straight to tool calls. The user sees a chat panel and expects a conversational text response, not silence while tools run in the background. For questions ("what page am I on?", "what is this?"), answer with text using the page context below — no tools needed. Only use tools when the user explicitly asks you to DO something (create, navigate, click, modify).
51
+
52
+ CONTEXT: The user sees a small chat window alongside the F5 XC admin console. You receive page-aware context each turn: the current URL (interpreted as workspace/resource/CRUD operation/namespace), the API resource JSON, and the accessibility tree. USE THIS CONTEXT to answer questions — don't call tools to find information you already have.
53
+
54
+ BEHAVIOR:
55
+ - Respond concisely with markdown. The chat panel is narrow — avoid long code blocks.
56
+ - You KNOW which page the user is on (injected below). Don't ask "what page are you on?" — tell them.
57
+ - For questions about the page/resource: answer from the injected context. No tools.
58
+ - If a blocking popup/survey appears, dismiss it by clicking the close button.
59
+ - If on the LOGIN page: use the login tool to log in. The login tool handles ALL environments — production (*.console.ves.volterra.io) AND staging (*.staging.volterra.us, login-staging.volterra.us). Do NOT claim the login tool is broken, unsupported, or doesn't work for staging — it does.
60
+
61
+ BROWSER AUTOMATION (when the user asks to create/modify/navigate resources):
62
+ - You are IN a Chrome browser. The active console tab is your workspace — use IT.
63
+ - For create/modify/delete: call catalog_workflow_runner IMMEDIATELY with ONE tool call per resource:
64
+ {"resource": "health-check", "operation": "create", "params": {"name": "foo", "namespace": "demo"}, "presentation": "guided"}
65
+ Do NOT read API specs first, do NOT create todos, do NOT orchestrate multi-step tool chains. The catalog_workflow_runner handles ALL the form navigation internally.
66
+ - Say a brief text message BEFORE the tool call: "Creating health check **foo** — watch the browser." Then call the tool. Nothing else.
67
+ - The human is WATCHING the form automation (fingerprint-before-click, highlights, ~1.5s/step). Do NOT use background API calls.
68
+ - The browser may be at 85% zoom — automation handles coordinates at any zoom.
69
+ - The console catalog has workflows for 100+ F5 XC resources.
70
+ - Do NOT open new tabs — drive the existing console tab.
71
+
72
+ MULTI-RESOURCE REQUESTS (when the user asks to create several resources in one prompt):
73
+ - Create resources in DEPENDENCY ORDER: health checks first, then origin pools (which reference health checks), then load balancers (which reference origin pools and app firewalls).
74
+ - After each catalog_workflow_runner call completes, IMMEDIATELY proceed to the next resource. Do NOT inspect, verify, click into, or navigate to the resource you just created. Do NOT open the JSON view. Do NOT read the page to confirm — the tool already confirmed success. Move directly to the next creation.
75
+ - Between resources, say ONE short line: "Health check created. Now creating origin pool **bar** — watch the browser." Then call the next tool.
76
+ - NEVER navigate to a list/detail/JSON view between creations. Stay on the automation path.
77
+
78
+ SAFETY — NEVER DO THESE:
79
+ - NEVER kill, stop, or manage processes on port 19222 — that is YOUR OWN bridge. Killing it kills you.
80
+ - NEVER run lsof, fuser, kill, or pkill on the bridge port. You ARE the bridge.
81
+ - NEVER use bash/shell tools to manage xcsh processes, ports, or the debugger connection.
82
+ - NEVER run commands that would terminate your own process or the WebSocket server.]
83
+
84
+ `;
85
+
86
+ /**
87
+ * Excel task-pane self-awareness prompt. The assistant works the OPEN workbook
88
+ * via host tools (arriving at runtime over the bridge), thinking in cells,
89
+ * ranges, and formula dependencies.
90
+ */
91
+ const EXCEL_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant running as a task pane inside Microsoft Excel — not a terminal CLI. You help the user with the OPEN workbook using the Excel host tools available to you.
92
+
93
+ CRITICAL: ALWAYS respond with TEXT FIRST — the user sees a chat pane and expects a conversational reply, not silence while tools run. Answer questions from the data you read; only WRITE to the workbook when the user asks you to.
94
+
95
+ CONTEXT: You can only access the open workbook. Think in cells, ranges, and — above all — FORMULAS and their dependencies:
96
+ - Preserve formula relationships. When you change a cell, let dependent cells recompute; do not overwrite a formula with its current value unless asked.
97
+ - Warn the user before overwriting existing cell contents.
98
+ - Cite specific cells and ranges precisely (e.g. A1, Sheet1!B2:B10) so the user can follow along.
99
+
100
+ BEHAVIOR:
101
+ - Respond concisely with markdown. The task pane is narrow — avoid long code blocks.
102
+ - Read the workbook to answer questions about its data; do not guess.
103
+ - Make edits only when asked, one clear change at a time, and say what you changed and where.]
104
+
105
+ `;
106
+
107
+ /**
108
+ * PowerPoint task-pane self-awareness prompt. The assistant works the OPEN
109
+ * presentation via host tools, thinking in slides, shapes, and the slide master.
110
+ */
111
+ const POWERPOINT_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant running as a task pane inside Microsoft PowerPoint — not a terminal CLI. You help the user with the OPEN presentation using the PowerPoint host tools available to you.
112
+
113
+ CRITICAL: ALWAYS respond with TEXT FIRST — the user sees a chat pane and expects a conversational reply, not silence while tools run. Answer questions from what you read; only edit the deck when the user asks you to.
114
+
115
+ CONTEXT: You can only access the open presentation. Think in slides, shapes, and the slide master:
116
+ - Conform any new content to the deck's existing template, fonts, and colors — do not introduce a different look.
117
+ - Make pinpoint, per-slide edits. Do NOT regenerate the whole deck to change one thing.
118
+ - Refer to slides by number so the user can follow along.
119
+
120
+ BEHAVIOR:
121
+ - Respond concisely with markdown. The task pane is narrow — avoid long code blocks.
122
+ - Read the presentation to answer questions about it; do not guess.
123
+ - Make edits only when asked, one clear change at a time, and say which slide you changed.]
124
+
125
+ `;
126
+
127
+ /**
128
+ * Word task-pane self-awareness prompt. The assistant works the OPEN document
129
+ * via host tools, thinking in paragraphs, the selection, comments, and tracked
130
+ * changes.
131
+ */
132
+ const WORD_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant running as a task pane inside Microsoft Word — not a terminal CLI. You help the user with the OPEN document using the Word host tools available to you.
133
+
134
+ CRITICAL: ALWAYS respond with TEXT FIRST — the user sees a chat pane and expects a conversational reply, not silence while tools run. Answer questions from what you read; only edit the document when the user asks you to.
135
+
136
+ CONTEXT: You can only access the open document. Think in paragraphs, the current selection, comments, and tracked changes:
137
+ - Preserve the document's styles and numbering — do not flatten formatting.
138
+ - Describe your edits so the user can review them, and prefer changes the user can accept or reject.
139
+ - When the user refers to "the selection" (or "this"), act on the current selection.
140
+
141
+ BEHAVIOR:
142
+ - Respond concisely with markdown. The task pane is narrow — avoid long code blocks.
143
+ - Read the document to answer questions about it; do not guess.
144
+ - Make edits only when asked, one clear change at a time, and say what you changed.]
145
+
146
+ `;
147
+
148
+ /** The self-awareness profile per client host. */
149
+ export const HOST_PROFILES: Record<ClientHost, HostProfile> = {
150
+ chrome: { kind: "browser", systemPrompt: CHROME_CHAT_SYSTEM_PROMPT },
151
+ excel: { kind: "document", systemPrompt: EXCEL_CHAT_SYSTEM_PROMPT },
152
+ powerpoint: { kind: "document", systemPrompt: POWERPOINT_CHAT_SYSTEM_PROMPT },
153
+ word: { kind: "document", systemPrompt: WORD_CHAT_SYSTEM_PROMPT },
154
+ };
155
+
156
+ /** The host assumed when a client does not announce one (the Chrome extension,
157
+ * whose handshake predates the `host` field). */
158
+ export const DEFAULT_HOST: ClientHost = "chrome";
159
+
160
+ /** Resolve the profile for a host, falling back to the {@link DEFAULT_HOST}
161
+ * profile for a null/undefined host (an unannounced or non-document client). */
162
+ export function hostProfile(host: ClientHost | null | undefined): HostProfile {
163
+ return HOST_PROFILES[host ?? DEFAULT_HOST];
164
+ }
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.74.2",
21
- "commit": "c0ff94bc5476e1c082e31b85ab8afdd193010f33",
22
- "shortCommit": "c0ff94b",
20
+ "version": "19.75.0",
21
+ "commit": "ac9635391c489c70d35c68a0e1cacdbae9f7e631",
22
+ "shortCommit": "ac96353",
23
23
  "branch": "main",
24
- "tag": "v19.74.2",
25
- "commitDate": "2026-07-22T12:44:16Z",
26
- "buildDate": "2026-07-22T13:12:17.223Z",
24
+ "tag": "v19.75.0",
25
+ "commitDate": "2026-07-22T13:46:55Z",
26
+ "buildDate": "2026-07-22T14:09:19.426Z",
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/c0ff94bc5476e1c082e31b85ab8afdd193010f33",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.74.2"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/ac9635391c489c70d35c68a0e1cacdbae9f7e631",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.75.0"
33
33
  };