@f5-sales-demo/xcsh 19.105.0 → 19.105.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [19.105.1] - 2026-07-31
6
+
7
+ ### Fixed
8
+
9
+ - Fixed GPT-5.6 Sol inference through LiteLLM when deterministic print mode requests an unsupported temperature ([#2698](https://github.com/f5-sales-demo/xcsh/issues/2698))
10
+
5
11
  ## [19.104.0] - 2026-07-31
6
12
 
7
13
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "19.105.0",
4
+ "version": "19.105.2",
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",
@@ -57,13 +57,13 @@
57
57
  "dependencies": {
58
58
  "@agentclientprotocol/sdk": "1.3.0",
59
59
  "@mozilla/readability": "^0.6",
60
- "@f5-sales-demo/xcsh-stats": "19.105.0",
61
- "@f5-sales-demo/pi-agent-core": "19.105.0",
62
- "@f5-sales-demo/pi-ai": "19.105.0",
63
- "@f5-sales-demo/pi-natives": "19.105.0",
64
- "@f5-sales-demo/pi-resource-management": "19.105.0",
65
- "@f5-sales-demo/pi-tui": "19.105.0",
66
- "@f5-sales-demo/pi-utils": "19.105.0",
60
+ "@f5-sales-demo/xcsh-stats": "19.105.2",
61
+ "@f5-sales-demo/pi-agent-core": "19.105.2",
62
+ "@f5-sales-demo/pi-ai": "19.105.2",
63
+ "@f5-sales-demo/pi-natives": "19.105.2",
64
+ "@f5-sales-demo/pi-resource-management": "19.105.2",
65
+ "@f5-sales-demo/pi-tui": "19.105.2",
66
+ "@f5-sales-demo/pi-utils": "19.105.2",
67
67
  "@sinclair/typebox": "^0.34",
68
68
  "@xterm/headless": "^6.0",
69
69
  "ajv": "^8.20",
@@ -43,8 +43,58 @@ const IS_BUN_COMPILED =
43
43
 
44
44
  // Dev-mode assets: packages/coding-agent/src/browser → packages/office-pane/dist.
45
45
  const DEV_DIST_DIR = path.resolve(import.meta.dir, "..", "..", "..", "office-pane", "dist");
46
+ /** The office-pane package itself. Present in a checkout, absent from a published tarball. */
47
+ const DEV_PACKAGE_DIR = path.dirname(DEV_DIST_DIR);
46
48
  const COMPILED_DIR_ROOT = path.join(os.tmpdir(), "xcsh-office-pane");
47
49
 
50
+ /**
51
+ * The files a usable pane must have regardless of what its manifest says.
52
+ *
53
+ * Not one marker. The build writes `taskpane.html` first and `manifest.json` three steps later, so a
54
+ * single marker accepts an interrupted build: `serve` would answer the page, then `manifest` and
55
+ * `sideload` would fail with the same ENOENT this check exists to replace.
56
+ */
57
+ const PANE_REQUIRED_FILES = ["taskpane.html", "taskpane.js", "manifest.json"] as const;
58
+
59
+ /**
60
+ * The assets a manifest says it needs, as dist-relative paths.
61
+ *
62
+ * Read out of the manifest rather than listed in this file, so adding an icon there cannot leave this
63
+ * check behind — the same "one map, or the copies drift" rule the rest of the pane follows.
64
+ *
65
+ * They are NOT cosmetic, which an earlier version of this guard assumed by grouping them with the
66
+ * fonts. `office sideload` shells out to office-addin-debugging, whose zip step fails outright on a
67
+ * missing `assets/color.png`; the comment in `runSideload` records exactly that. Since the build copies
68
+ * `assets/` after `manifest.json`, an interruption between the two leaves every other required file in
69
+ * place — and `runSideload` deletes the existing WEF registration before it discovers the problem.
70
+ *
71
+ * The manifest carries both forms, `assets/color.png` and an absolute `…:8444/assets/icon-16.png`, and
72
+ * both normalise to the same dist-relative path.
73
+ */
74
+ export function manifestAssetPaths(manifestText: string): string[] {
75
+ return [...new Set(manifestText.match(/assets\/[\w.-]+/g) ?? [])];
76
+ }
77
+
78
+ /** Which supply route was expected to provide the pane, and therefore what the remedy is. */
79
+ export type PaneSource = "compiled" | "dev" | "packaged";
80
+
81
+ /**
82
+ * There is no pane to serve, and it is the environment's doing rather than a bug.
83
+ *
84
+ * Its own class so the command boundary can print the remedy and exit 1, while a real defect keeps its
85
+ * stack trace. String-matching the message would couple the two and quietly swallow the next genuine
86
+ * failure whose text happened to look similar.
87
+ */
88
+ export class OfficePaneUnavailableError extends Error {
89
+ constructor(
90
+ message: string,
91
+ readonly source: PaneSource,
92
+ ) {
93
+ super(message);
94
+ this.name = "OfficePaneUnavailableError";
95
+ }
96
+ }
97
+
48
98
  const getEmbeddedArchive = (() => {
49
99
  const txt = embeddedPaneArchiveTxt.replaceAll(/[\s\r\n]/g, "").trim();
50
100
  if (!txt) return null;
@@ -81,31 +131,166 @@ async function extractEmbeddedArchive(archiveBytes: Buffer, outputDir: string):
81
131
  }
82
132
  }
83
133
 
134
+ /**
135
+ * Why there is no pane to serve, and what to do about it.
136
+ *
137
+ * Pure, and separate from the check, because the wording is the whole value of this path and the two
138
+ * audiences need opposite advice. Telling a developer to install a binary hides that they have simply
139
+ * not built yet; telling somebody who installed from npm to run a build names a directory they do not
140
+ * have. Getting that backwards is worse than the ENOENT it replaces.
141
+ */
142
+ export function paneUnavailableMessage(distDir: string, source: PaneSource): string {
143
+ if (source === "compiled") {
144
+ return (
145
+ "The Office pane is missing from this binary: office-pane.generated.txt carries no embedded " +
146
+ "archive. Rebuild the binary with the office-pane assets baked in."
147
+ );
148
+ }
149
+ if (source === "dev") {
150
+ return (
151
+ `The Office pane has not been built: ${distDir} does not exist. ` +
152
+ "Run `bun run build` in packages/office-pane, then try again."
153
+ );
154
+ }
155
+ return (
156
+ "The Office pane is not included in the npm package — it ships as a build-time asset of the " +
157
+ "compiled binary, which is also what provides the sideload and the trusted certificate. " +
158
+ "Install it with `brew install f5-sales-demo/tap/xcsh` and run `xcsh office` from there."
159
+ );
160
+ }
161
+
162
+ /**
163
+ * Which supply route a non-compiled run was relying on, from whether the office-pane package is here.
164
+ *
165
+ * A checkout has `packages/office-pane` and may simply not have built it; a published tarball has no
166
+ * `packages/` directory at all. One line, but the line that chooses between two opposite remedies, so
167
+ * it is named and tested rather than inlined as a ternary nothing covers.
168
+ */
169
+ export function paneSourceForLayout(officePanePackagePresent: boolean): PaneSource {
170
+ return officePanePackagePresent ? "dev" : "packaged";
171
+ }
172
+
173
+ /** The manifest's text if it parses as JSON, else null — presence alone says too little. */
174
+ async function readValidManifest(manifestPath: string): Promise<string | null> {
175
+ try {
176
+ const text = await Bun.file(manifestPath).text();
177
+ JSON.parse(text);
178
+ return text;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ /** Which of the files a usable pane needs are absent from `dir`. Empty means complete. */
185
+ export async function missingPaneFiles(dir: string): Promise<string[]> {
186
+ const missing: string[] = [];
187
+ for (const name of PANE_REQUIRED_FILES) {
188
+ if (!(await Bun.file(path.join(dir, name)).exists())) missing.push(name);
189
+ }
190
+ // Only worth asking once the manifest is there to be read — and a manifest that is present but
191
+ // unreadable is not a manifest. A truncated one has no asset references, so an existence-only check
192
+ // finds nothing missing, calls the pane complete, and lets `office manifest` print invalid JSON.
193
+ if (!missing.includes("manifest.json")) {
194
+ const manifestText = await readValidManifest(path.join(dir, "manifest.json"));
195
+ if (manifestText === null) {
196
+ missing.push("manifest.json (present but not valid JSON)");
197
+ } else {
198
+ for (const asset of manifestAssetPaths(manifestText)) {
199
+ if (!(await Bun.file(path.join(dir, asset)).exists())) missing.push(asset);
200
+ }
201
+ }
202
+ }
203
+ return missing;
204
+ }
205
+
206
+ /** True when `dir` holds every file a pane needs. */
207
+ export async function isCompletePane(dir: string): Promise<boolean> {
208
+ return (await missingPaneFiles(dir)).length === 0;
209
+ }
210
+
211
+ /**
212
+ * Return `dir` only if it actually holds a built pane, else refuse with {@link paneUnavailableMessage}.
213
+ *
214
+ * The dev branch used to return its path unchecked, which is how a published npm install came to bind
215
+ * :8444 and answer 404 to every request — Office rendered "Not Found" and nothing reported an error.
216
+ * Serving nothing quietly is the failure this exists to prevent.
217
+ */
218
+ export async function resolvePaneDir(dir: string, source: PaneSource): Promise<string> {
219
+ const missing = await missingPaneFiles(dir);
220
+ if (missing.length === 0) return dir;
221
+ // Name what is absent. "The pane is missing" sends someone looking at the whole directory; "no
222
+ // manifest.json" points at the step of the build that did not finish.
223
+ throw new OfficePaneUnavailableError(
224
+ `${paneUnavailableMessage(dir, source)} (missing: ${missing.join(", ")})`,
225
+ source,
226
+ );
227
+ }
228
+
229
+ /**
230
+ * Extract into a staging directory and move it into place only once it validates.
231
+ *
232
+ * Extraction used to write straight into the final hash-addressed directory, so an interruption or a
233
+ * full disk left a partial bundle exactly where the next run looks for a finished one — and every run
234
+ * after that accepted it, serving a pane whose JS 404s or whose script is half a file.
235
+ *
236
+ * Validating file CONTENTS would be an endless chase: a truncated `taskpane.js` is still valid text, so
237
+ * each new file type invites its own check. Publishing atomically closes the whole class instead. A
238
+ * crash leaves a `.incoming-*` directory, which the cache lookup ignores because it is not the name it
239
+ * asks for.
240
+ *
241
+ * The staging name carries the pid so two processes racing a first run cannot rename over each other
242
+ * mid-extraction.
243
+ */
244
+ export async function publishCompletePane(
245
+ outputDir: string,
246
+ extractInto: (stagingDir: string) => Promise<void>,
247
+ ): Promise<string> {
248
+ const stagingDir = `${outputDir}.incoming-${process.pid}`;
249
+ await fs.rm(stagingDir, { recursive: true, force: true });
250
+ await fs.mkdir(stagingDir, { recursive: true });
251
+ try {
252
+ await extractInto(stagingDir);
253
+ // Validate BEFORE publishing, so an incomplete extraction never becomes the cache.
254
+ await resolvePaneDir(stagingDir, "compiled");
255
+ await fs.rm(outputDir, { recursive: true, force: true });
256
+ await fs.rename(stagingDir, outputDir);
257
+ return outputDir;
258
+ } finally {
259
+ await fs.rm(stagingDir, { recursive: true, force: true });
260
+ }
261
+ }
262
+
84
263
  /**
85
264
  * Resolve the directory the assets are served from: the extracted embedded
86
265
  * bundle in a compiled binary, else `packages/office-pane/dist` in dev.
87
266
  */
88
267
  export async function getOfficePaneDir(): Promise<string> {
89
- if (!IS_BUN_COMPILED) return DEV_DIST_DIR;
268
+ if (!IS_BUN_COMPILED) {
269
+ const present = await Bun.file(path.join(DEV_PACKAGE_DIR, "package.json")).exists();
270
+ return resolvePaneDir(DEV_DIST_DIR, paneSourceForLayout(present));
271
+ }
90
272
  if (compiledDirPromise) return compiledDirPromise;
91
273
 
92
274
  const archiveBytes = getEmbeddedArchive?.();
93
275
  if (!archiveBytes) {
94
- throw new Error("Compiled office-pane bundle missing. Rebuild the binary with the embedded office-pane assets.");
276
+ throw new OfficePaneUnavailableError(paneUnavailableMessage(COMPILED_DIR_ROOT, "compiled"), "compiled");
95
277
  }
96
278
 
97
279
  compiledDirPromise = (async () => {
98
280
  const bundleHash = Bun.hash(archiveBytes).toString(16);
99
281
  const outputDir = path.join(COMPILED_DIR_ROOT, bundleHash);
100
- const markerPath = path.join(outputDir, "taskpane.html");
101
- try {
102
- if ((await fs.stat(markerPath)).isFile()) return outputDir;
103
- } catch {}
282
+ // A cached extraction is reused only if it is COMPLETE. The old check was `taskpane.html` alone,
283
+ // so an extraction interrupted after the page but before its bundle left a hash-addressed
284
+ // directory that every later run accepted — superseding a working server to serve a pane whose
285
+ // JS 404s, until somebody deleted a temp directory they had no reason to suspect.
286
+ //
287
+ // Incomplete is not fatal here, unlike in the dev case: the archive is in hand, so the remedy is
288
+ // to extract it again rather than to tell anyone anything.
289
+ if (await isCompletePane(outputDir)) return outputDir;
104
290
 
105
- await fs.rm(outputDir, { recursive: true, force: true });
106
- await fs.mkdir(outputDir, { recursive: true });
107
- await extractEmbeddedArchive(archiveBytes, outputDir);
108
- return outputDir;
291
+ // Still incomplete after a fresh extraction means the baked archive is itself short, which no
292
+ // amount of retrying fixes — publishCompletePane refuses rather than serving a pane with holes.
293
+ return publishCompletePane(outputDir, staging => extractEmbeddedArchive(archiveBytes, staging));
109
294
  })();
110
295
 
111
296
  return compiledDirPromise;
@@ -151,9 +336,13 @@ export interface OfficePaneServer {
151
336
  * Start the fixed :8444 HTTPS listener serving the embedded/dev assets. Resolves
152
337
  * TLS via {@link resolveBridgeTls} (the shared local-ip.sh cert). Serves only GET
153
338
  * (405 otherwise); unknown paths return 404.
339
+ *
340
+ * The asset directory is resolved BEFORE the port is bound and before TLS is fetched, so a run with
341
+ * no pane available refuses without holding :8444 and without reaching the network. `assetDir` is the
342
+ * seam that lets a test assert exactly that, in the same spirit as the pure `handleAssetRequest`.
154
343
  */
155
- export async function startOfficePaneServer(port = OFFICE_PANE_PORT): Promise<OfficePaneServer> {
156
- const dir = await getOfficePaneDir();
344
+ export async function startOfficePaneServer(port = OFFICE_PANE_PORT, assetDir?: string): Promise<OfficePaneServer> {
345
+ const dir = assetDir === undefined ? await getOfficePaneDir() : await resolvePaneDir(assetDir, "dev");
157
346
  const tls = await resolveBridgeTls();
158
347
 
159
348
  const server = Bun.serve({
@@ -17,6 +17,7 @@ import {
17
17
  getOfficePaneDir,
18
18
  OFFICE_PANE_PORT,
19
19
  type OfficePaneServer,
20
+ OfficePaneUnavailableError,
20
21
  readManifest,
21
22
  startOfficePaneServer,
22
23
  } from "../browser/office-pane-server";
@@ -55,9 +56,15 @@ export interface OfficeServeDeps {
55
56
  startOfficePaneServer: typeof startOfficePaneServer;
56
57
  startHeadlessChatBridge: typeof startHeadlessChatBridge;
57
58
  supersedeStaleServe: typeof supersedeStaleServe;
59
+ getOfficePaneDir: typeof getOfficePaneDir;
58
60
  }
59
61
 
60
- const defaultServeDeps: OfficeServeDeps = { startOfficePaneServer, startHeadlessChatBridge, supersedeStaleServe };
62
+ const defaultServeDeps: OfficeServeDeps = {
63
+ startOfficePaneServer,
64
+ startHeadlessChatBridge,
65
+ supersedeStaleServe,
66
+ getOfficePaneDir,
67
+ };
61
68
 
62
69
  /** A running `office serve`: the pane file server, the (optional) chat bridge, and
63
70
  * a teardown that disposes both. */
@@ -75,6 +82,12 @@ export interface OfficeServeHandle {
75
82
  * Extracted from {@link runServe} so the start/teardown wiring is unit-testable.
76
83
  */
77
84
  export async function startOfficeServe(deps: OfficeServeDeps = defaultServeDeps): Promise<OfficeServeHandle> {
85
+ // Prove there is a pane to serve BEFORE anything is torn down. supersedeStaleServe SIGTERMs a
86
+ // running serve to take the port, so checking afterwards let an install with no pane at all stop
87
+ // the operator's working one and then exit with nothing in its place. Refusing loudly is only an
88
+ // improvement if it refuses before it breaks something.
89
+ await deps.getOfficePaneDir();
90
+
78
91
  // Step down a stale serve squatting :8444 (e.g. left over from before a
79
92
  // `brew upgrade`) so this start binds cleanly instead of "port 8444 in use".
80
93
  const superseded = await deps.supersedeStaleServe(OFFICE_PANE_PORT);
@@ -238,6 +251,21 @@ export async function runOfficeCommand(
238
251
  args: OfficeCommandArgs,
239
252
  deps: OfficeCommandDeps = defaultCommandDeps,
240
253
  ): Promise<void> {
254
+ try {
255
+ await dispatchOfficeCommand(args, deps);
256
+ } catch (err) {
257
+ // Only this one class. The published npm form carries `office` and no pane bundle, so every
258
+ // action here can legitimately have nothing to work with — that is a fact about the install,
259
+ // not a defect, and it deserves the remedy rather than a stack trace. Anything else rethrows
260
+ // with its stack intact, because swallowing a real failure here is how the original silent
261
+ // 404 survived.
262
+ if (!(err instanceof OfficePaneUnavailableError)) throw err;
263
+ console.error(err.message);
264
+ process.exitCode = 1;
265
+ }
266
+ }
267
+
268
+ async function dispatchOfficeCommand(args: OfficeCommandArgs, deps: OfficeCommandDeps): Promise<void> {
241
269
  switch (args.action) {
242
270
  case "serve":
243
271
  await deps.serve();
@@ -140,6 +140,7 @@ const ReasoningEffortMapSchema = Type.Object({
140
140
 
141
141
  const OpenAICompatSchema = Type.Object({
142
142
  supportsStore: Type.Optional(Type.Boolean()),
143
+ supportsTemperature: Type.Optional(Type.Boolean()),
143
144
  supportsDeveloperRole: Type.Optional(Type.Boolean()),
144
145
  supportsReasoningEffort: Type.Optional(Type.Boolean()),
145
146
  reasoningEffortMap: Type.Optional(ReasoningEffortMapSchema),
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.105.0",
21
- "commit": "2df670b0ab2acc2e41d7574c0c40456abad497fd",
22
- "shortCommit": "2df670b",
20
+ "version": "19.105.2",
21
+ "commit": "3393e245e239a6aa875c405931a8814b1acb26a4",
22
+ "shortCommit": "3393e24",
23
23
  "branch": "main",
24
- "tag": "v19.105.0",
25
- "commitDate": "2026-07-31T14:20:29Z",
26
- "buildDate": "2026-07-31T14:50:23.668Z",
24
+ "tag": "v19.105.2",
25
+ "commitDate": "2026-07-31T16:38:06Z",
26
+ "buildDate": "2026-07-31T17:06:38.688Z",
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/2df670b0ab2acc2e41d7574c0c40456abad497fd",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.105.0"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/3393e245e239a6aa875c405931a8814b1acb26a4",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.105.2"
33
33
  };