@f5-sales-demo/xcsh 19.85.4 → 19.85.6
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.85.
|
|
4
|
+
"version": "19.85.6",
|
|
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.85.
|
|
60
|
-
"@f5-sales-demo/pi-agent-core": "19.85.
|
|
61
|
-
"@f5-sales-demo/pi-ai": "19.85.
|
|
62
|
-
"@f5-sales-demo/pi-natives": "19.85.
|
|
63
|
-
"@f5-sales-demo/pi-resource-management": "19.85.
|
|
64
|
-
"@f5-sales-demo/pi-tui": "19.85.
|
|
65
|
-
"@f5-sales-demo/pi-utils": "19.85.
|
|
59
|
+
"@f5-sales-demo/xcsh-stats": "19.85.6",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.85.6",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.85.6",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.85.6",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.85.6",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.85.6",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.85.6",
|
|
66
66
|
"@sinclair/typebox": "^0.34",
|
|
67
67
|
"@xterm/headless": "^6.0",
|
|
68
68
|
"ajv": "^8.20",
|
package/src/cli/office-cli.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* mirroring `stats-cli.ts` / `chrome-cli.ts`.
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as os from "node:os";
|
|
11
13
|
import * as path from "node:path";
|
|
12
14
|
import { LOCALIP_HOST } from "../browser/bridge-cert";
|
|
13
15
|
import { type HeadlessChatBridge, startHeadlessChatBridge } from "../browser/headless-bridge";
|
|
@@ -133,7 +135,49 @@ async function runServe(): Promise<void> {
|
|
|
133
135
|
});
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
/**
|
|
138
|
+
/** The macOS Office desktop containers whose `wef` folder holds sideloaded manifests. */
|
|
139
|
+
const OFFICE_WEF_CONTAINERS = ["com.microsoft.Excel", "com.microsoft.Powerpoint", "com.microsoft.Word"];
|
|
140
|
+
|
|
141
|
+
/** The `wef` sideload directories for the desktop Office apps under `homeDir`. */
|
|
142
|
+
export function officeWefDirs(homeDir: string): string[] {
|
|
143
|
+
return OFFICE_WEF_CONTAINERS.map(c => path.join(homeDir, "Library", "Containers", c, "Data", "Documents", "wef"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* `office-addin-debugging` symlinks the add-in manifest into each Office container's
|
|
148
|
+
* `wef` folder as `<manifestId>.manifest.json` and fails `EEXIST` if a link from a
|
|
149
|
+
* prior sideload is already there — so a repeat sideload errors even though the
|
|
150
|
+
* add-in registered fine. Remove any stale copy for our id so `office sideload` is
|
|
151
|
+
* idempotent. Best-effort per dir (a missing file or container is a no-op). Returns
|
|
152
|
+
* the paths actually removed.
|
|
153
|
+
*/
|
|
154
|
+
export function removeStaleWefManifests(manifestId: string, homeDir: string = os.homedir()): string[] {
|
|
155
|
+
const removed: string[] = [];
|
|
156
|
+
for (const dir of officeWefDirs(homeDir)) {
|
|
157
|
+
const p = path.join(dir, `${manifestId}.manifest.json`);
|
|
158
|
+
try {
|
|
159
|
+
if (fs.existsSync(p)) {
|
|
160
|
+
fs.rmSync(p);
|
|
161
|
+
removed.push(p);
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
/* best-effort: a permission error or race here must not block the sideload */
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return removed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The add-in manifest id from the bundled manifest.json (the `wef` link's basename). */
|
|
171
|
+
function manifestIdFrom(manifestText: string): string | undefined {
|
|
172
|
+
try {
|
|
173
|
+
const id = (JSON.parse(manifestText) as { id?: unknown }).id;
|
|
174
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
175
|
+
} catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Run the Office sideload against the embedded bundle (idempotent + best-effort). */
|
|
137
181
|
async function runSideload(app: OfficeApp): Promise<void> {
|
|
138
182
|
// Point office-addin-debugging at the extracted bundle dir, which has
|
|
139
183
|
// manifest.json AND its referenced `assets/` icons colocated. A bare temp
|
|
@@ -141,6 +185,21 @@ async function runSideload(app: OfficeApp): Promise<void> {
|
|
|
141
185
|
// step: `File to zip ".../assets/color.png" does not exist`.
|
|
142
186
|
const dir = await getOfficePaneDir();
|
|
143
187
|
const manifestPath = path.join(dir, "manifest.json");
|
|
188
|
+
|
|
189
|
+
// Idempotency: office-addin-debugging fails EEXIST if a prior sideload left a
|
|
190
|
+
// `<id>.manifest.json` link in a container's wef folder. Clear stale copies first.
|
|
191
|
+
const manifestId = manifestIdFrom(
|
|
192
|
+
await Bun.file(manifestPath)
|
|
193
|
+
.text()
|
|
194
|
+
.catch(() => ""),
|
|
195
|
+
);
|
|
196
|
+
if (manifestId) {
|
|
197
|
+
const removed = removeStaleWefManifests(manifestId);
|
|
198
|
+
if (removed.length > 0) {
|
|
199
|
+
console.log(`Cleared ${removed.length} stale sideload manifest link(s) from a previous sideload.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
144
203
|
console.log(`Sideloading ${manifestPath} into ${app} (requires the office-addin-debugging / atk tool on PATH)...`);
|
|
145
204
|
|
|
146
205
|
const result = spawnSync("office-addin-debugging", ["start", manifestPath, "desktop", "--app", app], {
|
|
@@ -82,13 +82,25 @@ class RpcHostToolAdapter<TParams extends TSchema = TSchema, TTheme extends Theme
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* How long a host tool may go with NO response — neither a `host_tool_result` nor a
|
|
87
|
+
* `host_tool_update` — before we give up on it. An unanswered call (e.g. an Office
|
|
88
|
+
* task-pane whose WebView the host suspended when it lost focus) would otherwise hang
|
|
89
|
+
* the agent turn forever, and since turns are queued behind the active one, every
|
|
90
|
+
* later turn wedges too. This is IDLE time, reset by each streamed update, so a
|
|
91
|
+
* legitimately slow-but-progressing tool is never cut off — only true silence trips it.
|
|
92
|
+
*/
|
|
93
|
+
export const HOST_TOOL_IDLE_TIMEOUT_MS = 60_000;
|
|
94
|
+
|
|
85
95
|
export class RpcHostToolBridge {
|
|
86
96
|
#output: RpcHostToolOutput;
|
|
87
97
|
#definitions = new Map<string, RpcHostToolDefinition>();
|
|
88
98
|
#pendingCalls = new Map<string, PendingHostToolCall>();
|
|
99
|
+
#idleTimeoutMs: number;
|
|
89
100
|
|
|
90
|
-
constructor(output: RpcHostToolOutput) {
|
|
101
|
+
constructor(output: RpcHostToolOutput, opts?: { idleTimeoutMs?: number }) {
|
|
91
102
|
this.#output = output;
|
|
103
|
+
this.#idleTimeoutMs = opts?.idleTimeoutMs ?? HOST_TOOL_IDLE_TIMEOUT_MS;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
getToolNames(): string[] {
|
|
@@ -141,7 +153,23 @@ export class RpcHostToolBridge {
|
|
|
141
153
|
const { promise, resolve, reject } = Promise.withResolvers<AgentToolResult<unknown>>();
|
|
142
154
|
let settled = false;
|
|
143
155
|
|
|
156
|
+
// ---- idle timer (reset by updates; fires → cancel + reject) ----
|
|
157
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
158
|
+
const clearIdle = (): void => {
|
|
159
|
+
if (idleTimer !== undefined) {
|
|
160
|
+
clearTimeout(idleTimer);
|
|
161
|
+
idleTimer = undefined;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const armIdle = (): void => {
|
|
165
|
+
clearIdle();
|
|
166
|
+
idleTimer = setTimeout(onIdleTimeout, this.#idleTimeoutMs);
|
|
167
|
+
// Don't keep the process alive just for this backstop.
|
|
168
|
+
if (typeof idleTimer === "object" && "unref" in idleTimer) idleTimer.unref();
|
|
169
|
+
};
|
|
170
|
+
|
|
144
171
|
const cleanup = () => {
|
|
172
|
+
clearIdle();
|
|
145
173
|
signal?.removeEventListener("abort", onAbort);
|
|
146
174
|
this.#pendingCalls.delete(id);
|
|
147
175
|
};
|
|
@@ -158,6 +186,23 @@ export class RpcHostToolBridge {
|
|
|
158
186
|
reject(new Error(`Host tool "${definition.name}" was aborted`));
|
|
159
187
|
};
|
|
160
188
|
|
|
189
|
+
const onIdleTimeout = () => {
|
|
190
|
+
if (settled) return;
|
|
191
|
+
settled = true;
|
|
192
|
+
cleanup();
|
|
193
|
+
this.#output({
|
|
194
|
+
type: "host_tool_cancel",
|
|
195
|
+
id: Snowflake.next() as string,
|
|
196
|
+
targetId: id,
|
|
197
|
+
});
|
|
198
|
+
reject(
|
|
199
|
+
new Error(
|
|
200
|
+
`Host tool "${definition.name}" did not respond within ${this.#idleTimeoutMs / 1000}s ` +
|
|
201
|
+
"(the host may be unresponsive — a suspended Office WebView loses its connection)",
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
161
206
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
162
207
|
this.#pendingCalls.set(id, {
|
|
163
208
|
resolve: result => {
|
|
@@ -172,7 +217,11 @@ export class RpcHostToolBridge {
|
|
|
172
217
|
cleanup();
|
|
173
218
|
reject(error);
|
|
174
219
|
},
|
|
175
|
-
onUpdate
|
|
220
|
+
onUpdate: partial => {
|
|
221
|
+
// A streamed update proves the host is alive — reset the idle clock.
|
|
222
|
+
armIdle();
|
|
223
|
+
onUpdate?.(partial);
|
|
224
|
+
},
|
|
176
225
|
});
|
|
177
226
|
|
|
178
227
|
this.#output({
|
|
@@ -183,6 +232,9 @@ export class RpcHostToolBridge {
|
|
|
183
232
|
arguments: args,
|
|
184
233
|
});
|
|
185
234
|
|
|
235
|
+
// Start the idle clock AFTER sending the call.
|
|
236
|
+
armIdle();
|
|
237
|
+
|
|
186
238
|
return promise;
|
|
187
239
|
}
|
|
188
240
|
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.85.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.85.6",
|
|
21
|
+
"commit": "58616737ba891a0d71cbdec7314292ed2efbd38f",
|
|
22
|
+
"shortCommit": "5861673",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.85.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.85.6",
|
|
25
|
+
"commitDate": "2026-07-23T23:21:56Z",
|
|
26
|
+
"buildDate": "2026-07-23T23:42:32.289Z",
|
|
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.85.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/58616737ba891a0d71cbdec7314292ed2efbd38f",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.85.6"
|
|
33
33
|
};
|