@f5-sales-demo/xcsh 19.72.1 → 19.74.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 +8 -8
- package/src/browser/extension-bridge-tools.ts +28 -0
- package/src/browser/headless-bridge.ts +164 -0
- package/src/browser/provider.ts +4 -1
- package/src/cli/office-cli.ts +73 -5
- package/src/commands/worker.ts +5 -23
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/system-prompt.plugin-pointer.test.ts +38 -0
- package/src/system-prompt.ts +14 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.74.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.
|
|
60
|
-
"@f5-sales-demo/pi-agent-core": "19.
|
|
61
|
-
"@f5-sales-demo/pi-ai": "19.
|
|
62
|
-
"@f5-sales-demo/pi-natives": "19.
|
|
63
|
-
"@f5-sales-demo/pi-resource-management": "19.
|
|
64
|
-
"@f5-sales-demo/pi-tui": "19.
|
|
65
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
59
|
+
"@f5-sales-demo/xcsh-stats": "19.74.0",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.74.0",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.74.0",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.74.0",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.74.0",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.74.0",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.74.0",
|
|
66
66
|
"@sinclair/typebox": "^0.34",
|
|
67
67
|
"@xterm/headless": "^6.0",
|
|
68
68
|
"ajv": "^8.20",
|
|
@@ -69,3 +69,31 @@ export function createExtensionBridgeTools(bridge: BridgeServer): CustomTool<TSc
|
|
|
69
69
|
export const EXTENSION_AGENT_TOOL_NAMES: readonly string[] = EXTENSION_CAPABILITIES.tools
|
|
70
70
|
.filter(def => !INTERNAL_TOOLS.has(def.name))
|
|
71
71
|
.map(def => def.name);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Builtin agent tools scoped into a headless browser-bridge session (the Chrome
|
|
75
|
+
* extension worker and the Office `serve` bridge). Shared so both bootstraps use
|
|
76
|
+
* one list. Office document tools are NOT here — the pane advertises those over
|
|
77
|
+
* the bridge via `set_host_tools`, registered at runtime by the ChatHandler.
|
|
78
|
+
*/
|
|
79
|
+
export const BROWSER_TOOL_NAMES: readonly string[] = [
|
|
80
|
+
"catalog_workflow_runner",
|
|
81
|
+
"navigate",
|
|
82
|
+
"click",
|
|
83
|
+
"click_element",
|
|
84
|
+
"fill",
|
|
85
|
+
"type_text",
|
|
86
|
+
"screenshot",
|
|
87
|
+
"login",
|
|
88
|
+
"read_ax",
|
|
89
|
+
"get_page_context",
|
|
90
|
+
"query_dom",
|
|
91
|
+
"find",
|
|
92
|
+
"wait_for",
|
|
93
|
+
"key_press",
|
|
94
|
+
"select_option",
|
|
95
|
+
"label_select",
|
|
96
|
+
"scroll_to",
|
|
97
|
+
"annotate",
|
|
98
|
+
"set_explain_mode",
|
|
99
|
+
];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless chat bridge — the no-TUI extension-bridge host used by `xcsh office
|
|
3
|
+
* serve` so one command yields a working task pane (pane files + a live chat/
|
|
4
|
+
* host-tool bridge). It mirrors the proven `xcsh worker` bootstrap
|
|
5
|
+
* (`commands/worker.ts`) MINUS the fleet concerns (manager keepalive, pre-warm
|
|
6
|
+
* IPC bind, TTFT spans): set the browser-provider env, init Settings + Context,
|
|
7
|
+
* quiet startup, provision the wss cert, start the bridge, publish session info,
|
|
8
|
+
* create ONE agent session scoped to the browser tools, and attach the
|
|
9
|
+
* `ChatHandler`. Office document tools (Excel/Word/PPT) are advertised by the
|
|
10
|
+
* pane at runtime over the bridge (`set_host_tools`), so they need no scoping here.
|
|
11
|
+
*
|
|
12
|
+
* The heavy / socket / network calls are injected (defaulting to the real ones)
|
|
13
|
+
* so the wiring is unit-testable without opening real listeners or a session.
|
|
14
|
+
*
|
|
15
|
+
* NOT browser-safe (node/bun): runs inside the full xcsh binary, never the pane.
|
|
16
|
+
*/
|
|
17
|
+
import { getProjectDir, getXCSHConfigDir } from "@f5-sales-demo/pi-utils";
|
|
18
|
+
import { createAgentSession } from "../sdk";
|
|
19
|
+
import { ContextService } from "../services/xcsh-context";
|
|
20
|
+
import { deriveTenantEnv } from "../services/xcsh-env";
|
|
21
|
+
import { resolveBridgeTls } from "./bridge-cert";
|
|
22
|
+
import { ChatHandler } from "./chat-handler";
|
|
23
|
+
import { type BridgeServer, startBridgeServer } from "./extension-bridge";
|
|
24
|
+
import { BROWSER_TOOL_NAMES, createExtensionBridgeTools, EXTENSION_AGENT_TOOL_NAMES } from "./extension-bridge-tools";
|
|
25
|
+
import { setSharedBridgeServer } from "./provider";
|
|
26
|
+
|
|
27
|
+
/** A running headless bridge + a teardown that disposes the chat handler and
|
|
28
|
+
* closes the bridge (both ws + wss listeners). */
|
|
29
|
+
export interface HeadlessChatBridge {
|
|
30
|
+
bridge: BridgeServer;
|
|
31
|
+
dispose: () => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Tenant identity for the `hello` handshake, contextless-friendly: the active
|
|
36
|
+
* `/context` wins (its apiUrl + name), otherwise fall back to the `XCSH_API_URL`/
|
|
37
|
+
* `XCSH_SESSION_TENANT` env so the pane still learns which tenant this process
|
|
38
|
+
* serves. Sync — the bridge calls it while answering `hello`. Mirrors the
|
|
39
|
+
* interactive path in `main.ts` (there is no per-tab session id here).
|
|
40
|
+
*/
|
|
41
|
+
export function sessionInfoForOfficeServe(): {
|
|
42
|
+
tenant: string | null;
|
|
43
|
+
env: string | null;
|
|
44
|
+
apiUrl: string | null;
|
|
45
|
+
contextBound: boolean;
|
|
46
|
+
sessionId: string | null;
|
|
47
|
+
} {
|
|
48
|
+
let apiUrl: string | null = null;
|
|
49
|
+
let contextBound = false;
|
|
50
|
+
try {
|
|
51
|
+
apiUrl = ContextService.instance.activeApiUrl;
|
|
52
|
+
contextBound = ContextService.instance.getStatus().activeContextName != null;
|
|
53
|
+
} catch {
|
|
54
|
+
/* ContextService not initialized — env-only mode. */
|
|
55
|
+
}
|
|
56
|
+
apiUrl = apiUrl ?? process.env.XCSH_API_URL ?? null;
|
|
57
|
+
const tenantKey = process.env.XCSH_SESSION_TENANT ?? null;
|
|
58
|
+
const { tenant, env } = deriveTenantEnv(apiUrl, tenantKey);
|
|
59
|
+
return { tenant, env, apiUrl, contextBound, sessionId: null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Injectable seams (defaulted to the real ones) so the bootstrap is testable. */
|
|
63
|
+
export interface HeadlessBridgeDeps {
|
|
64
|
+
/** Set the browser-provider env, init Settings + ContextService + provider
|
|
65
|
+
* persistence, and quiet startup; returns the project cwd for the session. */
|
|
66
|
+
initEnv: () => Promise<{ cwd: string }>;
|
|
67
|
+
resolveBridgeTls: typeof resolveBridgeTls;
|
|
68
|
+
startBridgeServer: typeof startBridgeServer;
|
|
69
|
+
setSharedBridgeServer: typeof setSharedBridgeServer;
|
|
70
|
+
createExtensionBridgeTools: typeof createExtensionBridgeTools;
|
|
71
|
+
createAgentSession: typeof createAgentSession;
|
|
72
|
+
ChatHandlerCtor: typeof ChatHandler;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const defaultDeps: HeadlessBridgeDeps = {
|
|
76
|
+
initEnv: async () => {
|
|
77
|
+
process.env.XCSH_BROWSER_PROVIDER = "extension";
|
|
78
|
+
const cwd = getProjectDir();
|
|
79
|
+
const { Settings, settings } = await import("../config/settings");
|
|
80
|
+
await Settings.init({ cwd });
|
|
81
|
+
// Init the ContextService singleton so sessionInfoForOfficeServe can read the
|
|
82
|
+
// active apiUrl once a /context is bound.
|
|
83
|
+
try {
|
|
84
|
+
ContextService.init(getXCSHConfigDir());
|
|
85
|
+
} catch {
|
|
86
|
+
/* already initialized / unavailable — continue. */
|
|
87
|
+
}
|
|
88
|
+
// Provider persistence for model discovery (parity with main.ts / worker.ts).
|
|
89
|
+
const { initializeWithSettings } = await import("../discovery");
|
|
90
|
+
initializeWithSettings(settings);
|
|
91
|
+
// Quiet startup: skip the welcome screen + blocking plugin "Fix now?" prompts.
|
|
92
|
+
settings.override("startup.quiet", true);
|
|
93
|
+
return { cwd };
|
|
94
|
+
},
|
|
95
|
+
resolveBridgeTls,
|
|
96
|
+
startBridgeServer,
|
|
97
|
+
setSharedBridgeServer,
|
|
98
|
+
createExtensionBridgeTools,
|
|
99
|
+
createAgentSession,
|
|
100
|
+
ChatHandlerCtor: ChatHandler,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Start the headless chat bridge and return it with a teardown. Fully awaits the
|
|
105
|
+
* session + ChatHandler.attach() before resolving, so once this resolves the pane
|
|
106
|
+
* can connect and chat immediately (no warm-up race). A `configure`-less pane
|
|
107
|
+
* chats over xcsh's already-configured provider.
|
|
108
|
+
*/
|
|
109
|
+
export async function startHeadlessChatBridge(deps: HeadlessBridgeDeps = defaultDeps): Promise<HeadlessChatBridge> {
|
|
110
|
+
const { cwd } = await deps.initEnv();
|
|
111
|
+
|
|
112
|
+
// Provision the wss cert before binding (warm boot = on-disk cache hit);
|
|
113
|
+
// `undefined` (offline) → the bridge starts ws-only.
|
|
114
|
+
const tls = await deps.resolveBridgeTls();
|
|
115
|
+
const bridge = await deps.startBridgeServer(undefined, tls ? { tls } : undefined);
|
|
116
|
+
// Reuse this bridge for any in-process selectProvider() (no conflicting second bridge).
|
|
117
|
+
deps.setSharedBridgeServer(bridge);
|
|
118
|
+
bridge.setSessionInfo(sessionInfoForOfficeServe);
|
|
119
|
+
// Re-announce the tenant when the active context changes (best-effort).
|
|
120
|
+
try {
|
|
121
|
+
ContextService.onContextChange(() => bridge.broadcastTenantChanged());
|
|
122
|
+
} catch {
|
|
123
|
+
/* ContextService not initialized (tests) — the tenant is static. */
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Everything past the bind can throw (createAgentSession on a misconfigured
|
|
127
|
+
// provider, etc.). If it does, close the already-bound bridge and clear the
|
|
128
|
+
// shared-bridge global before rethrowing — otherwise the ws/wss listeners leak
|
|
129
|
+
// (keeping the event loop alive so Ctrl+C can't exit) and a later in-process
|
|
130
|
+
// selectProvider() reuses a dead bridge. The caller (startOfficeServe) treats
|
|
131
|
+
// the rethrow as a non-fatal "pane only" fallback.
|
|
132
|
+
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);
|
|
137
|
+
const { session } = await deps.createAgentSession({
|
|
138
|
+
cwd,
|
|
139
|
+
hasUI: false,
|
|
140
|
+
toolNames: [...new Set([...BROWSER_TOOL_NAMES, ...EXTENSION_AGENT_TOOL_NAMES])],
|
|
141
|
+
customTools: extensionTools,
|
|
142
|
+
// Headless: no MCP/LSP/extension discovery — lean, no network/blocking prompts.
|
|
143
|
+
enableMCP: false,
|
|
144
|
+
enableLsp: false,
|
|
145
|
+
disableExtensionDiscovery: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const chatHandler = new deps.ChatHandlerCtor(bridge, session);
|
|
149
|
+
chatHandler.attach();
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
bridge,
|
|
153
|
+
dispose: async () => {
|
|
154
|
+
chatHandler.dispose();
|
|
155
|
+
deps.setSharedBridgeServer(null);
|
|
156
|
+
await bridge.close();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
} catch (err) {
|
|
160
|
+
deps.setSharedBridgeServer(null);
|
|
161
|
+
await bridge.close();
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/browser/provider.ts
CHANGED
|
@@ -115,7 +115,10 @@ export class CdpBrowserProvider implements BrowserProvider {
|
|
|
115
115
|
* instead of starting a conflicting second one on the same port.
|
|
116
116
|
*/
|
|
117
117
|
let _sharedBridgeServer: import("./extension-bridge").BridgeServer | null = null;
|
|
118
|
-
|
|
118
|
+
/** Publish (or, with `null`, clear) the process-shared bridge. Clearing is
|
|
119
|
+
* required when a partially-started bridge is torn down (e.g. session bootstrap
|
|
120
|
+
* failed after bind) so a later `selectProvider()` never reuses a closed bridge. */
|
|
121
|
+
export function setSharedBridgeServer(server: import("./extension-bridge").BridgeServer | null): void {
|
|
119
122
|
_sharedBridgeServer = server;
|
|
120
123
|
}
|
|
121
124
|
|
package/src/cli/office-cli.ts
CHANGED
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { LOCALIP_HOST } from "../browser/bridge-cert";
|
|
13
|
+
import { type HeadlessChatBridge, startHeadlessChatBridge } from "../browser/headless-bridge";
|
|
14
|
+
import {
|
|
15
|
+
getOfficePaneDir,
|
|
16
|
+
type OfficePaneServer,
|
|
17
|
+
readManifest,
|
|
18
|
+
startOfficePaneServer,
|
|
19
|
+
} from "../browser/office-pane-server";
|
|
13
20
|
|
|
14
21
|
/** The subcommands `xcsh office` accepts (also the Args `options` constraint). */
|
|
15
22
|
export const OFFICE_ACTIONS = ["serve", "manifest", "sideload"] as const;
|
|
@@ -39,9 +46,53 @@ export async function writeManifest(outPath?: string): Promise<string> {
|
|
|
39
46
|
return text;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
/**
|
|
49
|
+
/** Injectable seams for {@link startOfficeServe} (defaulted to the real ones). */
|
|
50
|
+
export interface OfficeServeDeps {
|
|
51
|
+
startOfficePaneServer: typeof startOfficePaneServer;
|
|
52
|
+
startHeadlessChatBridge: typeof startHeadlessChatBridge;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const defaultServeDeps: OfficeServeDeps = { startOfficePaneServer, startHeadlessChatBridge };
|
|
56
|
+
|
|
57
|
+
/** A running `office serve`: the pane file server, the (optional) chat bridge, and
|
|
58
|
+
* a teardown that disposes both. */
|
|
59
|
+
export interface OfficeServeHandle {
|
|
60
|
+
server: OfficePaneServer;
|
|
61
|
+
chat: HeadlessChatBridge | null;
|
|
62
|
+
dispose: () => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Start BOTH the :8444 pane file server AND the headless chat bridge, so one
|
|
67
|
+
* `xcsh office serve` yields a working pane (no separately-run bridge). A bridge
|
|
68
|
+
* failure is NON-fatal: the pane still serves and we warn, so `serve` degrades to
|
|
69
|
+
* "pane only" rather than failing outright. Returns a teardown that disposes both.
|
|
70
|
+
* Extracted from {@link runServe} so the start/teardown wiring is unit-testable.
|
|
71
|
+
*/
|
|
72
|
+
export async function startOfficeServe(deps: OfficeServeDeps = defaultServeDeps): Promise<OfficeServeHandle> {
|
|
73
|
+
const server = await deps.startOfficePaneServer();
|
|
74
|
+
let chat: HeadlessChatBridge | null = null;
|
|
75
|
+
try {
|
|
76
|
+
chat = await deps.startHeadlessChatBridge();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`Warning: the chat bridge could not start (${err instanceof Error ? err.message : String(err)}). ` +
|
|
80
|
+
"The task pane will load but chat is unavailable until a bridge is running.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
server,
|
|
85
|
+
chat,
|
|
86
|
+
dispose: async () => {
|
|
87
|
+
if (chat) await chat.dispose();
|
|
88
|
+
server.stop();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Start the pane server + chat bridge, print status, and block until killed. */
|
|
43
94
|
async function runServe(): Promise<void> {
|
|
44
|
-
const server = await
|
|
95
|
+
const { server, chat, dispose } = await startOfficeServe();
|
|
45
96
|
console.log(`Serving the xcsh Office task pane at ${server.taskpaneUrl}`);
|
|
46
97
|
if (!server.trusted) {
|
|
47
98
|
console.warn(
|
|
@@ -49,9 +100,26 @@ async function runServe(): Promise<void> {
|
|
|
49
100
|
"Office's WebView may refuse to load the page until the cert is trusted.",
|
|
50
101
|
);
|
|
51
102
|
}
|
|
103
|
+
if (chat) {
|
|
104
|
+
if (chat.bridge.wssPort) {
|
|
105
|
+
console.log(
|
|
106
|
+
`Chat bridge ready on wss://${LOCALIP_HOST}:${chat.bridge.wssPort} — the pane connects automatically.`,
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
console.warn(
|
|
110
|
+
"Warning: the chat bridge is ws-only (no wss cert). The pane connects over wss and may not reach it.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
52
114
|
console.log("Press Ctrl+C to stop.");
|
|
53
|
-
//
|
|
54
|
-
await new Promise<
|
|
115
|
+
// Block until a signal tears us down, then dispose the bridge + pane server.
|
|
116
|
+
await new Promise<void>(resolve => {
|
|
117
|
+
const shutdown = (): void => {
|
|
118
|
+
void dispose().finally(resolve);
|
|
119
|
+
};
|
|
120
|
+
process.once("SIGINT", shutdown);
|
|
121
|
+
process.once("SIGTERM", shutdown);
|
|
122
|
+
});
|
|
55
123
|
}
|
|
56
124
|
|
|
57
125
|
/** Run the Office sideload against the embedded bundle (best-effort). */
|
package/src/commands/worker.ts
CHANGED
|
@@ -20,7 +20,11 @@ import { Command } from "@f5-sales-demo/pi-utils/cli";
|
|
|
20
20
|
import { LOCALIP_HOST, resolveBridgeTls } from "../browser/bridge-cert";
|
|
21
21
|
import { ChatHandler } from "../browser/chat-handler";
|
|
22
22
|
import { startBridgeServer } from "../browser/extension-bridge";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
BROWSER_TOOL_NAMES,
|
|
25
|
+
createExtensionBridgeTools,
|
|
26
|
+
EXTENSION_AGENT_TOOL_NAMES,
|
|
27
|
+
} from "../browser/extension-bridge-tools";
|
|
24
28
|
import { setSharedBridgeServer } from "../browser/provider";
|
|
25
29
|
import { coldStartSpans, type SpanFrame, sessionBuildSpan } from "../browser/ttft-spans";
|
|
26
30
|
import { initializeWithSettings } from "../discovery";
|
|
@@ -94,28 +98,6 @@ function managerSockPath(): string {
|
|
|
94
98
|
/** Browser-automation tool set — identical scoping to `main.ts`'s extension path.
|
|
95
99
|
* With scoped tools the ONLY way to create a resource is the form-driven workflow
|
|
96
100
|
* runner, which is exactly what the human watching the browser wants. */
|
|
97
|
-
const BROWSER_TOOL_NAMES = [
|
|
98
|
-
"catalog_workflow_runner",
|
|
99
|
-
"navigate",
|
|
100
|
-
"click",
|
|
101
|
-
"click_element",
|
|
102
|
-
"fill",
|
|
103
|
-
"type_text",
|
|
104
|
-
"screenshot",
|
|
105
|
-
"login",
|
|
106
|
-
"read_ax",
|
|
107
|
-
"get_page_context",
|
|
108
|
-
"query_dom",
|
|
109
|
-
"find",
|
|
110
|
-
"wait_for",
|
|
111
|
-
"key_press",
|
|
112
|
-
"select_option",
|
|
113
|
-
"label_select",
|
|
114
|
-
"scroll_to",
|
|
115
|
-
"annotate",
|
|
116
|
-
"set_explain_mode",
|
|
117
|
-
];
|
|
118
|
-
|
|
119
101
|
export default class Worker extends Command {
|
|
120
102
|
static description = "Run a headless extension-bridge worker (no TUI); blocks until SIGTERM";
|
|
121
103
|
|
|
@@ -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.74.0",
|
|
21
|
+
"commit": "c6fc2dd6c16c816e5857edc7e892837405dead7a",
|
|
22
|
+
"shortCommit": "c6fc2dd",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.74.0",
|
|
25
|
+
"commitDate": "2026-07-22T05:55:19Z",
|
|
26
|
+
"buildDate": "2026-07-22T06:19:41.705Z",
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/c6fc2dd6c16c816e5857edc7e892837405dead7a",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.74.0"
|
|
33
33
|
};
|
|
@@ -306,6 +306,9 @@ Most tools resolve custom protocol URLs to internal resources (not web URLs):
|
|
|
306
306
|
- `xcsh://console/<resource>` — console route pattern, menu path, and available operations.
|
|
307
307
|
- `xcsh://console/<resource>/<operation>` — the exact ordered UI steps (selectors) for that operation.
|
|
308
308
|
- `xcsh://extension` — Chrome extension bridge tool API reference: which tool to use (click, typeahead, input, navigation) for each automation task.
|
|
309
|
+
{{#if hasPlugins}}
|
|
310
|
+
Installed plugins expose capabilities, schemas, and executable helpers on demand. Read `xcsh://plugin` to list installed plugins and `xcsh://plugin/<name>` for a plugin's summary and how to go deeper — pull this in only when a task matches a plugin's domain.
|
|
311
|
+
{{/if}}
|
|
309
312
|
|
|
310
313
|
### Presentation profile
|
|
311
314
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { prompt } from "@f5-sales-demo/pi-utils";
|
|
4
|
+
import { registerCodingAgentPromptHelpers } from "./config/prompt-templates";
|
|
5
|
+
|
|
6
|
+
// L0 of the MEDDPICC progressive-hints ladder: a single framework-agnostic line
|
|
7
|
+
// telling the agent that installed plugins expose capabilities/schemas via
|
|
8
|
+
// `xcsh://plugin/<name>`. It is gated on the `hasPlugins` render variable that
|
|
9
|
+
// `buildSystemPrompt` computes from discovered plugin roots.
|
|
10
|
+
//
|
|
11
|
+
// Seam: the Handlebars-compile path (`prompt.render(template, data)`) — the same
|
|
12
|
+
// seam every other conditional-rendering assertion in this package uses. It is
|
|
13
|
+
// deterministic (no dependency on what is installed under the real home dir) and
|
|
14
|
+
// exercises the exact template conditional this task adds. `buildSystemPrompt`
|
|
15
|
+
// resolves plugins from `os.homedir()` and takes no `home` override, so a
|
|
16
|
+
// true-case fixture through it would be non-deterministic.
|
|
17
|
+
|
|
18
|
+
const systemPromptPath = path.resolve(import.meta.dir, "prompts/system/system-prompt.md");
|
|
19
|
+
|
|
20
|
+
describe("L0 plugin-capability pointer", () => {
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
registerCodingAgentPromptHelpers();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("renders the generic pointer when a plugin is present, naming no plugin", async () => {
|
|
26
|
+
const template = await Bun.file(systemPromptPath).text();
|
|
27
|
+
const rendered = prompt.render(template, { hasPlugins: true });
|
|
28
|
+
expect(rendered).toContain("xcsh://plugin");
|
|
29
|
+
// Generic/reusable: it must not name any specific plugin.
|
|
30
|
+
expect(rendered).not.toContain("meddpicc");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("omits the pointer entirely when no plugins are present", async () => {
|
|
34
|
+
const template = await Bun.file(systemPromptPath).text();
|
|
35
|
+
const rendered = prompt.render(template, { hasPlugins: false });
|
|
36
|
+
expect(rendered).not.toContain("xcsh://plugin");
|
|
37
|
+
});
|
|
38
|
+
});
|
package/src/system-prompt.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { systemPromptCapability } from "./capability/system-prompt";
|
|
|
13
13
|
import type { SkillsSettings } from "./config/settings";
|
|
14
14
|
import { renderDeprecationGuardrails } from "./deprecations";
|
|
15
15
|
import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
|
|
16
|
+
import { listXcshPluginRoots } from "./discovery/helpers";
|
|
16
17
|
import { isApplicableToContext, loadSkills, type Skill } from "./extensibility/skills";
|
|
17
18
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
18
19
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
@@ -555,6 +556,13 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
555
556
|
? loadSkills({ ...mergedSkillsSettings, cwd: resolvedCwd }).then(result => result.skills)
|
|
556
557
|
: Promise.resolve([]);
|
|
557
558
|
|
|
559
|
+
// L0 plugin-capability pointer: gate a generic `xcsh://plugin` hint on ≥1
|
|
560
|
+
// discoverable plugin. Discovery is cached and reads one small registry file,
|
|
561
|
+
// so this is effectively free; it fails safe to `false` (no hint) on error.
|
|
562
|
+
const hasPluginsPromise: Promise<boolean> = listXcshPluginRoots(os.homedir(), resolvedCwd)
|
|
563
|
+
.then(result => result.roots.length > 0)
|
|
564
|
+
.catch(() => false);
|
|
565
|
+
|
|
558
566
|
return Promise.all([
|
|
559
567
|
resolvePromptInput(customPrompt, "system prompt"),
|
|
560
568
|
resolvePromptInput(appendSystemPrompt, "append system prompt"),
|
|
@@ -562,6 +570,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
562
570
|
contextFilesPromise,
|
|
563
571
|
agentsMdSearchPromise,
|
|
564
572
|
skillsPromise,
|
|
573
|
+
hasPluginsPromise,
|
|
565
574
|
]).then(
|
|
566
575
|
([
|
|
567
576
|
resolvedCustomPrompt,
|
|
@@ -570,6 +579,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
570
579
|
contextFiles,
|
|
571
580
|
agentsMdSearch,
|
|
572
581
|
skills,
|
|
582
|
+
hasPlugins,
|
|
573
583
|
]) => ({
|
|
574
584
|
resolvedCustomPrompt,
|
|
575
585
|
resolvedAppendPrompt,
|
|
@@ -577,6 +587,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
577
587
|
contextFiles,
|
|
578
588
|
agentsMdSearch,
|
|
579
589
|
skills,
|
|
590
|
+
hasPlugins,
|
|
580
591
|
}),
|
|
581
592
|
);
|
|
582
593
|
})();
|
|
@@ -601,6 +612,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
601
612
|
files: [],
|
|
602
613
|
};
|
|
603
614
|
let skills: Skill[] = providedSkills ?? [];
|
|
615
|
+
let hasPlugins = false;
|
|
604
616
|
|
|
605
617
|
if (prepResult.type === "timeout") {
|
|
606
618
|
logger.warn("System prompt preparation timed out; using minimal startup context", {
|
|
@@ -623,6 +635,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
623
635
|
contextFiles = dedupeExactContextFiles(prepResult.value.contextFiles);
|
|
624
636
|
agentsMdSearch = prepResult.value.agentsMdSearch;
|
|
625
637
|
skills = prepResult.value.skills;
|
|
638
|
+
hasPlugins = prepResult.value.hasPlugins;
|
|
626
639
|
}
|
|
627
640
|
|
|
628
641
|
const date = new Date().toISOString().slice(0, 10);
|
|
@@ -679,6 +692,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
679
692
|
agentsMdSearch,
|
|
680
693
|
skills: contextFilteredSkills,
|
|
681
694
|
rules: rules ?? [],
|
|
695
|
+
hasPlugins,
|
|
682
696
|
alwaysApplyRules: injectedAlwaysApplyRules,
|
|
683
697
|
date,
|
|
684
698
|
dateTime,
|