dsh-connect-workbuddy 3.4.2 → 3.4.4
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 +65 -1
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/WINDOWS.md +6 -0
- package/lib/bin.js +12 -4
- package/lib/client.js +128 -7
- package/lib/{host-heartbeat-Sq5JwluI.js → host-heartbeat-DTuwptkM.js} +245 -6
- package/lib/index.d.ts +130 -12
- package/lib/index.js +215 -15
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createDecipheriv, createHash } from "node:crypto";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { dirname, join, win32 } from "node:path";
|
|
5
5
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
6
6
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
7
7
|
import { execFile, execFileSync } from "node:child_process";
|
|
@@ -202,6 +202,167 @@ function macosBundleExecutable(bundle) {
|
|
|
202
202
|
return join(bundle, "Contents", "MacOS", name);
|
|
203
203
|
}
|
|
204
204
|
/**
|
|
205
|
+
* The executable path WorkBuddy's own uninstall registration points at, or
|
|
206
|
+
* undefined when the app was never registered.
|
|
207
|
+
*
|
|
208
|
+
* Why this exists: the four directory candidates in
|
|
209
|
+
* {@link workbuddyAppExecutableCandidates} encode the DEFAULT install layout.
|
|
210
|
+
* An app installed anywhere else — a second drive (the observed case was
|
|
211
|
+
* `E:\workbuddy\WorkBuddy.exe`) — is installed and signed in, yet every
|
|
212
|
+
* candidate misses, so the encrypted credential file cannot be opened and a
|
|
213
|
+
* user who IS signed in is reported as signed out. The README's answer was to
|
|
214
|
+
* set `WORKBUDDY_APP_EXECUTABLE` by hand; the installer already recorded the
|
|
215
|
+
* answer, so this reads it instead.
|
|
216
|
+
*
|
|
217
|
+
* The registration is the app's own claim about itself, which is what makes it
|
|
218
|
+
* safe to hand the path to the credential probe: a display name alone would be
|
|
219
|
+
* a guess, whereas the path here was written by the installer that placed the
|
|
220
|
+
* binary. The value is still checked against {@link APP_EXECUTABLE_NAME} and for
|
|
221
|
+
* existence before use, and the callers fall through to the documented hint when
|
|
222
|
+
* it is absent.
|
|
223
|
+
*
|
|
224
|
+
* The read is done through ALL THREE uninstall views, because the hive a
|
|
225
|
+
* registration lands in depends on how the app was installed: per-user
|
|
226
|
+
* (`/currentuser`, the layout WorkBuddy uses) registers under `HKCU`, a
|
|
227
|
+
* machine-wide install under `HKLM`, and a 32-bit machine-wide one under
|
|
228
|
+
* `HKLM\...\WOW6432Node`. A hive that does not exist or is unreadable is the
|
|
229
|
+
* normal case on a machine without the app, not an error: every failure path
|
|
230
|
+
* returns undefined so the caller can try the next candidate.
|
|
231
|
+
*/
|
|
232
|
+
function windowsRegistryAppExecutable(query = queryRegistry) {
|
|
233
|
+
for (const root of [
|
|
234
|
+
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
235
|
+
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
236
|
+
"HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
|
|
237
|
+
]) {
|
|
238
|
+
const listing = query([
|
|
239
|
+
"query",
|
|
240
|
+
root,
|
|
241
|
+
"/s",
|
|
242
|
+
"/f",
|
|
243
|
+
"WorkBuddy",
|
|
244
|
+
"/t",
|
|
245
|
+
"REG_SZ"
|
|
246
|
+
]);
|
|
247
|
+
if (listing === void 0) continue;
|
|
248
|
+
const executable = registryExecutableFromQuery(listing);
|
|
249
|
+
if (executable !== void 0) return executable;
|
|
250
|
+
const fromDirectory = registryInstallLocationFromQuery(listing);
|
|
251
|
+
if (fromDirectory !== void 0) return fromDirectory;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Parse `reg query` output for the value that names the installed executable.
|
|
256
|
+
*
|
|
257
|
+
* `reg query` prints keys as `HKEY_CURRENT_USER\...\Uninstall\<id>` followed by
|
|
258
|
+
* indented ` ValueName REG_SZ Data` lines. Only `DisplayIcon` is
|
|
259
|
+
* trusted here: it is the one value the installer writes pointing AT the
|
|
260
|
+
* binary. The value is normalised by {@link normalizeRegistryExecutable}, which
|
|
261
|
+
* strips the quotes and the icon index and rejects anything that is not the
|
|
262
|
+
* desktop executable.
|
|
263
|
+
*/
|
|
264
|
+
function registryExecutableFromQuery(output) {
|
|
265
|
+
for (const rawLine of output.split(/\r?\n/u)) {
|
|
266
|
+
const match = /^\s*DisplayIcon\s+REG_SZ\s+(.*?)\s*$/u.exec(rawLine);
|
|
267
|
+
if (match === null) continue;
|
|
268
|
+
const executable = normalizeRegistryExecutable(match[1]);
|
|
269
|
+
if (executable !== void 0) return executable;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The `WorkBuddy.exe` under an `InstallLocation`-style registry directory.
|
|
274
|
+
*
|
|
275
|
+
* Kept separate from {@link registryExecutableFromQuery} because the two values
|
|
276
|
+
* mean different things: `DisplayIcon` NAMES a file, whereas the directory has
|
|
277
|
+
* to have {@link APP_EXECUTABLE_NAME} joined onto it, and the result must still
|
|
278
|
+
* be checked for existence by the caller's probe.
|
|
279
|
+
*
|
|
280
|
+
* `win32.join` rather than the host's `join`: the input is a Windows registry
|
|
281
|
+
* value on EVERY machine this function can be called on. `path.join` is the
|
|
282
|
+
* running platform's, so a suite on Linux/macOS would mangle `E:\workbuddy`
|
|
283
|
+
* into `E:\workbuddy/WorkBuddy.exe` and the caller's `existsSync` could never
|
|
284
|
+
* match it. The separator here is fixed by the DATA, not by the host.
|
|
285
|
+
*/
|
|
286
|
+
function registryInstallLocationFromQuery(output) {
|
|
287
|
+
for (const rawLine of output.split(/\r?\n/u)) {
|
|
288
|
+
const match = /^\s*InstallLocation\s+REG_SZ\s+(.*?)\s*$/u.exec(rawLine);
|
|
289
|
+
if (match === null) continue;
|
|
290
|
+
const directory = stripRegistryQuotes(match[1]);
|
|
291
|
+
if (directory === void 0 || directory === "") continue;
|
|
292
|
+
return win32.join(directory, APP_EXECUTABLE_NAME);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* A registry executable value reduced to a usable path, or undefined when it
|
|
297
|
+
* cannot be one.
|
|
298
|
+
*
|
|
299
|
+
* `DisplayIcon` is stored as `"E:\workbuddy\WorkBuddy.exe",0`. The quotes and
|
|
300
|
+
* the icon index are stripped, and a value that names something other than the
|
|
301
|
+
* desktop executable — an `.ico`, an uninstaller, another product's binary — is
|
|
302
|
+
* rejected rather than launched. A bare path without quotes is accepted too,
|
|
303
|
+
* because not every installer writes the indexed form.
|
|
304
|
+
*
|
|
305
|
+
* The basename test uses `win32.basename` for the reason given on
|
|
306
|
+
* {@link registryInstallLocationFromQuery}: the separator belongs to the
|
|
307
|
+
* registry value (always Windows), so the host's `basename` — which does not
|
|
308
|
+
* treat `\` as a separator — would return the whole string and reject every
|
|
309
|
+
* legitimate path.
|
|
310
|
+
*/
|
|
311
|
+
function normalizeRegistryExecutable(value) {
|
|
312
|
+
const unquoted = stripRegistryQuotes(value);
|
|
313
|
+
if (unquoted === void 0 || unquoted === "") return void 0;
|
|
314
|
+
const withoutIndex = unquoted.replace(/,\s*-?\d+\s*$/u, "");
|
|
315
|
+
if (withoutIndex === "") return void 0;
|
|
316
|
+
if (win32.basename(withoutIndex).toLowerCase() !== APP_EXECUTABLE_NAME.toLowerCase()) return void 0;
|
|
317
|
+
return withoutIndex;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The path inside a `DisplayIcon`/`InstallLocation` value, unquoted.
|
|
321
|
+
*
|
|
322
|
+
* Registry data may be written either quoted (`"E:\workbuddy\WorkBuddy.exe",0`)
|
|
323
|
+
* or bare. Only a leading quote is treated as quoting: a bare `C:\dir\a"b.exe`
|
|
324
|
+
* is a real (if unusual) filename and must not be truncated at the quote.
|
|
325
|
+
*/
|
|
326
|
+
function stripRegistryQuotes(value) {
|
|
327
|
+
const trimmed = value.trim();
|
|
328
|
+
if (trimmed === "") return void 0;
|
|
329
|
+
if (!trimmed.startsWith("\"")) return trimmed;
|
|
330
|
+
const closing = trimmed.indexOf("\"", 1);
|
|
331
|
+
if (closing === -1) return void 0;
|
|
332
|
+
return trimmed.slice(1, closing).trim();
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Run `reg query` and return its stdout, or undefined when it cannot be run.
|
|
336
|
+
*
|
|
337
|
+
* `windowsHide` is REQUIRED, not cosmetic: the DSH Desktop host is an Electron
|
|
338
|
+
* GUI process with no console, so spawning a console program without it flashes
|
|
339
|
+
* a visible black window on every probe. The same reason is recorded for the
|
|
340
|
+
* heartbeat probe in `host-heartbeat.ts` and in `docs/WINDOWS.md` §1-3.
|
|
341
|
+
*
|
|
342
|
+
* `execFileSync` (not `execFile`) keeps this synchronous so the candidates list
|
|
343
|
+
* stays a pure function of its inputs: it is called during `findWorkbuddyApp`
|
|
344
|
+
* `Executable`, which `readAtRestKey()` and the `doctor` command both treat as
|
|
345
|
+
* a plain lookup. The read is bounded by the registry query's own output and
|
|
346
|
+
* happens at most once per discovery.
|
|
347
|
+
*/
|
|
348
|
+
function queryRegistry(args) {
|
|
349
|
+
try {
|
|
350
|
+
return execFileSync("reg", [...args], REGISTRY_PROBE_OPTIONS);
|
|
351
|
+
} catch {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Options for the registry probe; `windowsHide` is mandatory (see above), and
|
|
357
|
+
* the timeout matches `fetchAtRestKeyPayload()`'s: a hung `reg` must not wedge
|
|
358
|
+
* the synchronous discovery path forever.
|
|
359
|
+
*/
|
|
360
|
+
const REGISTRY_PROBE_OPTIONS = {
|
|
361
|
+
encoding: "utf8",
|
|
362
|
+
windowsHide: true,
|
|
363
|
+
timeout: 1e4
|
|
364
|
+
};
|
|
365
|
+
/**
|
|
205
366
|
* Candidate paths of the WorkBuddy desktop executable, in probe order.
|
|
206
367
|
*
|
|
207
368
|
* The Windows build is the one that encrypts credentials, so Windows leads;
|
|
@@ -224,7 +385,7 @@ function workbuddyAppExecutableCandidates(platform = process.platform, home = ho
|
|
|
224
385
|
const local = env["LOCALAPPDATA"]?.trim();
|
|
225
386
|
const programFiles = env["ProgramFiles"]?.trim();
|
|
226
387
|
const programFilesX86 = env["ProgramFiles(x86)"]?.trim();
|
|
227
|
-
candidates.push(local === void 0 || local === "" ? void 0 : join(local, "Programs", "WorkBuddy", APP_EXECUTABLE_NAME), local === void 0 || local === "" ? void 0 : join(local, "WorkBuddy", APP_EXECUTABLE_NAME), programFiles === void 0 || programFiles === "" ? void 0 : join(programFiles, "WorkBuddy", APP_EXECUTABLE_NAME), programFilesX86 === void 0 || programFilesX86 === "" ? void 0 : join(programFilesX86, "WorkBuddy", APP_EXECUTABLE_NAME));
|
|
388
|
+
candidates.push(local === void 0 || local === "" ? void 0 : win32.join(local, "Programs", "WorkBuddy", APP_EXECUTABLE_NAME), local === void 0 || local === "" ? void 0 : win32.join(local, "WorkBuddy", APP_EXECUTABLE_NAME), programFiles === void 0 || programFiles === "" ? void 0 : win32.join(programFiles, "WorkBuddy", APP_EXECUTABLE_NAME), programFilesX86 === void 0 || programFilesX86 === "" ? void 0 : win32.join(programFilesX86, "WorkBuddy", APP_EXECUTABLE_NAME));
|
|
228
389
|
} else if (platform === "darwin") for (const name of MACOS_APP_BUNDLE_NAMES) candidates.push(readBundleExecutable(join("/Applications", name)), readBundleExecutable(join(home, "Applications", name)));
|
|
229
390
|
return candidates.filter((candidate) => candidate !== void 0 && candidate !== "");
|
|
230
391
|
}
|
|
@@ -298,13 +459,55 @@ function macosNestedAppBundles(parent) {
|
|
|
298
459
|
return bundles;
|
|
299
460
|
}
|
|
300
461
|
/**
|
|
462
|
+
* Locate the desktop executable AND say how it was found.
|
|
463
|
+
*
|
|
464
|
+
* Kept beside {@link findWorkbuddyAppExecutable} rather than folded into it
|
|
465
|
+
* because the two answer different questions: callers on the credential path
|
|
466
|
+
* only want the path, whereas `doctor` must explain the discovery. Sharing the
|
|
467
|
+
* candidate list keeps them from drifting.
|
|
468
|
+
*/
|
|
469
|
+
function findWorkbuddyAppExecutableWithSource(platform = process.platform, home = homedir(), env = process.env, readRegistryAppPath = windowsRegistryAppExecutable) {
|
|
470
|
+
const fromEnv = env[WORKBUDDY_APP_EXECUTABLE_ENV]?.trim();
|
|
471
|
+
for (const candidate of workbuddyAppExecutableCandidates(platform, home, env)) {
|
|
472
|
+
try {
|
|
473
|
+
if (!existsSync(candidate)) continue;
|
|
474
|
+
} catch {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
executable: candidate,
|
|
479
|
+
source: fromEnv !== void 0 && fromEnv !== "" && candidate === fromEnv ? "env" : "default-layout"
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (platform === "win32") {
|
|
483
|
+
const fromRegistry = readRegistryAppPath();
|
|
484
|
+
if (fromRegistry !== void 0) try {
|
|
485
|
+
if (existsSync(fromRegistry)) return {
|
|
486
|
+
executable: fromRegistry,
|
|
487
|
+
source: "registry"
|
|
488
|
+
};
|
|
489
|
+
} catch {}
|
|
490
|
+
}
|
|
491
|
+
const executable = findWorkbuddyAppExecutable(platform, home, env, readRegistryAppPath);
|
|
492
|
+
return executable === void 0 ? void 0 : {
|
|
493
|
+
executable,
|
|
494
|
+
source: "nested-bundle"
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
301
498
|
* The first candidate that exists as a file, or undefined when the desktop app
|
|
302
499
|
* is not installed where this platform expects it.
|
|
303
500
|
*/
|
|
304
|
-
function findWorkbuddyAppExecutable(platform = process.platform, home = homedir(), env = process.env) {
|
|
501
|
+
function findWorkbuddyAppExecutable(platform = process.platform, home = homedir(), env = process.env, readRegistryAppPath = windowsRegistryAppExecutable) {
|
|
305
502
|
for (const candidate of workbuddyAppExecutableCandidates(platform, home, env)) try {
|
|
306
503
|
if (existsSync(candidate)) return candidate;
|
|
307
504
|
} catch {}
|
|
505
|
+
if (platform === "win32") {
|
|
506
|
+
const fromRegistry = readRegistryAppPath();
|
|
507
|
+
if (fromRegistry !== void 0) try {
|
|
508
|
+
if (existsSync(fromRegistry)) return fromRegistry;
|
|
509
|
+
} catch {}
|
|
510
|
+
}
|
|
308
511
|
if (platform === "darwin") for (const parent of ["/Applications", join(home, "Applications")]) for (const bundle of macosNestedAppBundles(parent)) {
|
|
309
512
|
const executable = macosBundleExecutable(bundle);
|
|
310
513
|
if (executable === void 0) continue;
|
|
@@ -449,12 +652,19 @@ const HARD_CREDIT_MARKERS = [
|
|
|
449
652
|
];
|
|
450
653
|
/** Session-invalidation markers that mean "sign in again in the WorkBuddy app". */
|
|
451
654
|
const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
|
|
655
|
+
/**
|
|
656
|
+
* Content-policy refusal markers from the chat gateway. The quoted-key form
|
|
657
|
+
* avoids substring hits inside unrelated values (request ids are hex and can
|
|
658
|
+
* contain `11140` by coincidence).
|
|
659
|
+
*/
|
|
660
|
+
const POLICY_REJECT_MARKERS = ["request illegal", "\"code\":11140"];
|
|
452
661
|
/** Classify an upstream failure from its HTTP status and body excerpt. */
|
|
453
662
|
function classifyUpstreamError(status, body) {
|
|
454
663
|
if (status === 402) return "hard_credit";
|
|
455
664
|
const lower = body.toLowerCase();
|
|
456
665
|
for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
|
|
457
666
|
for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
|
|
667
|
+
for (const marker of POLICY_REJECT_MARKERS) if (body.includes(marker)) return "policy_reject";
|
|
458
668
|
if (status === 429) return "soft_rate";
|
|
459
669
|
if (status === 404) return "not_found";
|
|
460
670
|
if (status >= 500) return "server";
|
|
@@ -462,6 +672,34 @@ function classifyUpstreamError(status, body) {
|
|
|
462
672
|
return "client";
|
|
463
673
|
}
|
|
464
674
|
/**
|
|
675
|
+
* Parse the known-good fields out of an upstream failure body. Non-JSON
|
|
676
|
+
* bodies (HTML error pages, empty strings) yield undefined rather than a guess.
|
|
677
|
+
*/
|
|
678
|
+
function parseUpstreamErrorDetail(body) {
|
|
679
|
+
let parsed;
|
|
680
|
+
try {
|
|
681
|
+
parsed = JSON.parse(body);
|
|
682
|
+
} catch {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
686
|
+
const raw = parsed;
|
|
687
|
+
let displayMsg;
|
|
688
|
+
const display = raw.displayMsg;
|
|
689
|
+
if (typeof display === "string") displayMsg = display;
|
|
690
|
+
else if (typeof display === "object" && display !== null) {
|
|
691
|
+
const localized = display;
|
|
692
|
+
const pick = localized.zh ?? localized.en;
|
|
693
|
+
if (typeof pick === "string") displayMsg = pick;
|
|
694
|
+
}
|
|
695
|
+
if (displayMsg === void 0 && typeof raw.msg === "string") displayMsg = raw.msg;
|
|
696
|
+
const detail = {};
|
|
697
|
+
if (typeof raw.code === "number") detail.upstreamCode = raw.code;
|
|
698
|
+
if (typeof raw.requestId === "string") detail.requestId = raw.requestId;
|
|
699
|
+
if (displayMsg !== void 0) detail.displayMsg = displayMsg;
|
|
700
|
+
return detail;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
465
703
|
* Region for a login domain; an empty domain means CN (matching upstream tooling).
|
|
466
704
|
*
|
|
467
705
|
* The international product is reachable under TWO brand domains: the WorkBuddy
|
|
@@ -944,7 +1182,8 @@ var WorkBuddyUpstreamClient = class {
|
|
|
944
1182
|
ok: false,
|
|
945
1183
|
status: response.status,
|
|
946
1184
|
kind: classifyUpstreamError(response.status, text),
|
|
947
|
-
message: text
|
|
1185
|
+
message: text,
|
|
1186
|
+
detail: parseUpstreamErrorDetail(text)
|
|
948
1187
|
};
|
|
949
1188
|
}
|
|
950
1189
|
/**
|
|
@@ -2552,7 +2791,7 @@ function generationOf(version) {
|
|
|
2552
2791
|
//#endregion
|
|
2553
2792
|
//#region src/version.ts
|
|
2554
2793
|
/** The npm package version this build was produced from. */
|
|
2555
|
-
const WORKBUDDY_CONNECT_VERSION = "3.4.
|
|
2794
|
+
const WORKBUDDY_CONNECT_VERSION = "3.4.4";
|
|
2556
2795
|
//#endregion
|
|
2557
2796
|
//#region src/host-heartbeat.ts
|
|
2558
2797
|
/**
|
|
@@ -2710,4 +2949,4 @@ async function clearHostHeartbeat() {
|
|
|
2710
2949
|
await rm(workbuddyHostHeartbeatPath(), { force: true });
|
|
2711
2950
|
}
|
|
2712
2951
|
//#endregion
|
|
2713
|
-
export {
|
|
2952
|
+
export { openEncryptedField as $, CREDENTIAL_REJECTED_CODE as A, prepareChatBody as B, defaultDesktopAuthPath as C, parseWorkBuddyAuth as D, legacyWorkbuddyOwnAuthPath as E, declaredTools as F, deriveAtRestKeyId as G, WORKBUDDY_APP_EXECUTABLE_ENV as H, isCredentialRejectedError as I, findWorkbuddyAppExecutableWithSource as J, fetchAtRestKeyPayload as K, parseCreditMultiplier as L, WorkBuddyCredentialRejectedError as M, WorkBuddyUpstreamClient as N, workbuddyAccountId as O, classifyUpstreamError as P, macosNestedAppBundles as Q, parseReasoning as R, defaultDesktopAuthDirs as S, isEncryptedCredentialError as T, clearAtRestKeyCache as U, regionOf as V, deriveAtRestKey as W, isWorkbuddyBundle as X, isEncryptedFieldWrapper as Y, macosBundleExecutable as Z, WORKBUDDY_AUTH_FILE_ENV as _, readHostHeartbeat as a, authFileName as b, WORKBUDDY_CONNECT_VERSION as c, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, readAtRestKey as et, WorkBuddyCatalog as f, WORKBUDDY_AUTH_FILENAME as g, ENCRYPTED_CREDENTIAL_CODE as h, processStartTimeMs as i, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as j, workbuddyOwnAuthPath as k, piAiRuntimeInfo as l, fallbackModelsFor as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, deriveCatalog as p, findWorkbuddyAppExecutable as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, workbuddyAppExecutableCandidates as tt, FALLBACK_WORKBUDDY_MODELS as u, WorkBuddyCredentialStore as v, hasEncryptedCredentialFields as w, defaultDesktopAuthCandidates as x, WorkBuddyEncryptedCredentialError as y, parseUpstreamModel as z };
|
package/lib/index.d.ts
CHANGED
|
@@ -435,7 +435,7 @@ declare class WorkBuddyCredentialStore {
|
|
|
435
435
|
/** WorkBuddy region selected by the credential's login domain. */
|
|
436
436
|
type WorkBuddyRegion = 'cn' | 'global';
|
|
437
437
|
/** Upstream failure classes the shim maps onto distinct HTTP answers. */
|
|
438
|
-
type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'not_found' | 'server' | 'client';
|
|
438
|
+
type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'policy_reject' | 'not_found' | 'server' | 'client';
|
|
439
439
|
/** Reasoning capability as the upstream catalog declares it. */
|
|
440
440
|
interface WorkBuddyReasoning {
|
|
441
441
|
supportedEfforts?: readonly string[];
|
|
@@ -532,6 +532,8 @@ type WorkBuddyChatResult = {
|
|
|
532
532
|
status: number;
|
|
533
533
|
kind: UpstreamErrorKind;
|
|
534
534
|
message: string;
|
|
535
|
+
/** Structured fields lifted from a JSON failure body, when there were any. */
|
|
536
|
+
detail?: UpstreamErrorDetail | undefined;
|
|
535
537
|
};
|
|
536
538
|
/**
|
|
537
539
|
* Chat answer that also carries the response headers.
|
|
@@ -554,6 +556,15 @@ interface WorkBuddyProbeAnswer {
|
|
|
554
556
|
}
|
|
555
557
|
/** Classify an upstream failure from its HTTP status and body excerpt. */
|
|
556
558
|
declare function classifyUpstreamError(status: number, body: string): UpstreamErrorKind;
|
|
559
|
+
/** Structured fields lifted out of a JSON upstream failure body. */
|
|
560
|
+
interface UpstreamErrorDetail {
|
|
561
|
+
/** Numeric upstream error code, e.g. 11140 for a content-policy refusal. */
|
|
562
|
+
upstreamCode?: number;
|
|
563
|
+
/** Upstream request id — the handle support asks for. */
|
|
564
|
+
requestId?: string;
|
|
565
|
+
/** Best human-facing message: `displayMsg.zh`, falling back to `.en`, then `msg`. */
|
|
566
|
+
displayMsg?: string;
|
|
567
|
+
}
|
|
557
568
|
/**
|
|
558
569
|
* Region for a login domain; an empty domain means CN (matching upstream tooling).
|
|
559
570
|
*
|
|
@@ -1170,7 +1181,7 @@ declare function macosNestedAppBundles(parent: string): string[];
|
|
|
1170
1181
|
* The first candidate that exists as a file, or undefined when the desktop app
|
|
1171
1182
|
* is not installed where this platform expects it.
|
|
1172
1183
|
*/
|
|
1173
|
-
declare function findWorkbuddyAppExecutable(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
1184
|
+
declare function findWorkbuddyAppExecutable(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv, readRegistryAppPath?: () => string | undefined): string | undefined;
|
|
1174
1185
|
/**
|
|
1175
1186
|
* Ask the installed desktop app for its key payload by running its own binary
|
|
1176
1187
|
* as plain Node (`ELECTRON_RUN_AS_NODE`) and calling the native binding.
|
|
@@ -1340,7 +1351,7 @@ declare const PROBE_SYSTEM_PROMPT = "You are a connectivity check. Reply with a
|
|
|
1340
1351
|
* Output cap for a probe. One token is enough to prove the model answers.
|
|
1341
1352
|
*
|
|
1342
1353
|
* The probe's cost comes from its INPUT, not its output (see
|
|
1343
|
-
* {@link
|
|
1354
|
+
* {@link DEFAULT_PROBE_INPUT_TOKENS}), so paying for a long completion would add money
|
|
1344
1355
|
* without adding signal.
|
|
1345
1356
|
*/
|
|
1346
1357
|
declare const PROBE_MAX_TOKENS = 1;
|
|
@@ -1367,8 +1378,27 @@ declare const PROBE_MAX_TOKENS = 1;
|
|
|
1367
1378
|
* A probe that sent a handful of tokens answered "usable" while every real
|
|
1368
1379
|
* request in a long conversation was refused — the probe was asking too small a
|
|
1369
1380
|
* question. 25k is sized to be a realistic long-conversation payload.
|
|
1381
|
+
*
|
|
1382
|
+
* Renamed from `PROBE_INPUT_TOKENS` to say what it now IS: the DEFAULT, not the
|
|
1383
|
+
* only possible size. The size is a property of the user's own traffic (see
|
|
1384
|
+
* {@link probeRequestBody}), so it became a setting; this constant is what an
|
|
1385
|
+
* unset setting, an older profile, and every existing test mean by "a probe".
|
|
1386
|
+
*/
|
|
1387
|
+
declare const DEFAULT_PROBE_INPUT_TOKENS = 25000;
|
|
1388
|
+
/**
|
|
1389
|
+
* The probe sizes the card offers, in tokens.
|
|
1390
|
+
*
|
|
1391
|
+
* A closed set, each step a meaningful position relative to the measured 20k~30k
|
|
1392
|
+
* threshold: below it (10k/20k) proves an account can serve SHORT requests and
|
|
1393
|
+
* is the cheap choice; at it (30k) is the size that finally covers the whole
|
|
1394
|
+
* measured window; above it (50k/100k) is for conversations that routinely run
|
|
1395
|
+
* long, where a smaller probe would report "usable" for an account that refuses
|
|
1396
|
+
* the very next message.
|
|
1397
|
+
*
|
|
1398
|
+
* Exported so the schema's description, the card's dropdown, and the coercion
|
|
1399
|
+
* below cannot drift apart into three different menus.
|
|
1370
1400
|
*/
|
|
1371
|
-
declare const
|
|
1401
|
+
declare const PROBE_INPUT_TOKEN_CHOICES: readonly number[];
|
|
1372
1402
|
/**
|
|
1373
1403
|
* Build the request body for one probe.
|
|
1374
1404
|
*
|
|
@@ -1376,10 +1406,13 @@ declare const PROBE_INPUT_TOKENS = 25000;
|
|
|
1376
1406
|
* `prepareChatBody` enforces that too, but sending it explicitly keeps this
|
|
1377
1407
|
* function's output valid on its own.
|
|
1378
1408
|
*
|
|
1379
|
-
* Measured on the built artifact: 112,713 bytes of body, and
|
|
1380
|
-
* counted `prompt_tokens: 25023` at a cost of `credit: 0.72`.
|
|
1409
|
+
* Measured on the built artifact at the DEFAULT size: 112,713 bytes of body, and
|
|
1410
|
+
* the upstream counted `prompt_tokens: 25023` at a cost of `credit: 0.72`. The
|
|
1411
|
+
* body bytes and the credit both scale with the size asked for, which is why the
|
|
1412
|
+
* size is a user setting: a larger probe answers more truthfully for someone
|
|
1413
|
+
* whose conversations are long, and costs proportionally more to ask.
|
|
1381
1414
|
*/
|
|
1382
|
-
declare function probeRequestBody(modelId: string): string;
|
|
1415
|
+
declare function probeRequestBody(modelId: string, inputTokens?: number): string;
|
|
1383
1416
|
/**
|
|
1384
1417
|
* How one probe ended.
|
|
1385
1418
|
*
|
|
@@ -1387,7 +1420,7 @@ declare function probeRequestBody(modelId: string): string;
|
|
|
1387
1420
|
* outcome the feature exists for: it means "this model works, but not right
|
|
1388
1421
|
* now". Everything else is a real, actionable failure or a success.
|
|
1389
1422
|
*/
|
|
1390
|
-
type WorkBuddyProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1423
|
+
type WorkBuddyProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1391
1424
|
/**
|
|
1392
1425
|
* One probe result, as the card renders it.
|
|
1393
1426
|
*
|
|
@@ -1455,9 +1488,10 @@ declare function cooldownOf(input: {
|
|
|
1455
1488
|
*
|
|
1456
1489
|
* Built on `classifyUpstreamError` rather than re-testing status codes, so a
|
|
1457
1490
|
* probe and a real chat request can never disagree about what the same upstream
|
|
1458
|
-
* answer means. The extra
|
|
1491
|
+
* answer means. The extra splits here are `credential-rejected`, which the
|
|
1459
1492
|
* upstream signals with a non-JSON 401/403 edge page and which needs completely
|
|
1460
|
-
* different advice (re-auth, not "wait")
|
|
1493
|
+
* different advice (re-auth, not "wait"), and `policy-rejected`, a JSON 403
|
|
1494
|
+
* content-policy refusal where re-auth advice would be actively misleading.
|
|
1461
1495
|
*/
|
|
1462
1496
|
declare function outcomeOfFailure(status: number, body: string): WorkBuddyProbeOutcome;
|
|
1463
1497
|
/**
|
|
@@ -1497,12 +1531,35 @@ declare function probeModel(input: {
|
|
|
1497
1531
|
credential: WorkBuddyCredential;
|
|
1498
1532
|
modelId: string;
|
|
1499
1533
|
nowMs: number;
|
|
1534
|
+
/**
|
|
1535
|
+
* How much input this probe sends, in tokens.
|
|
1536
|
+
*
|
|
1537
|
+
* Omitted means {@link DEFAULT_PROBE_INPUT_TOKENS}, so every existing caller
|
|
1538
|
+
* keeps the measured 25k and only the pool's user-facing "test" passes a
|
|
1539
|
+
* chosen size through.
|
|
1540
|
+
*/
|
|
1541
|
+
inputTokens?: number;
|
|
1500
1542
|
/** The region's next monthly quota refresh, when known. */
|
|
1501
1543
|
quotaRefreshAtMs?: number;
|
|
1502
1544
|
signal?: AbortSignal;
|
|
1503
1545
|
}): Promise<WorkBuddyProbeResult>;
|
|
1504
1546
|
//#endregion
|
|
1505
1547
|
//#region src/account-pool.d.ts
|
|
1548
|
+
/**
|
|
1549
|
+
* Which of the pool's writers produced a measurement.
|
|
1550
|
+
*
|
|
1551
|
+
* `test-batch` — the user pressed "test" (or the check-in batch), so the
|
|
1552
|
+
* outcome is a deliberate measurement of the target model.
|
|
1553
|
+
* `live-request` — a real chat request failed and its failure was recorded so
|
|
1554
|
+
* the NEXT request starts from a measurement instead of rediscovering the
|
|
1555
|
+
* same wall.
|
|
1556
|
+
*
|
|
1557
|
+
* The distinction is diagnostic, not behavioural: both feed the same routing
|
|
1558
|
+
* and cooling logic. It exists so a user who watched an account measure `ok`
|
|
1559
|
+
* and later saw it limited can tell whether that was a later live failure
|
|
1560
|
+
* (expected) or an older value winning (a bug).
|
|
1561
|
+
*/
|
|
1562
|
+
type WorkBuddyPoolProbeSource = 'test-batch' | 'live-request';
|
|
1506
1563
|
/** One account's pool entry: what the picker needs, plus its last measurement. */
|
|
1507
1564
|
interface WorkBuddyPoolAccount {
|
|
1508
1565
|
/** Stable account id (the same one `WorkBuddyAccountChoice` carries). */
|
|
@@ -1522,6 +1579,26 @@ interface WorkBuddyPoolProbe {
|
|
|
1522
1579
|
outcome: WorkBuddyProbeOutcome;
|
|
1523
1580
|
/** When this measurement was taken, in epoch ms. */
|
|
1524
1581
|
atMs: number;
|
|
1582
|
+
/**
|
|
1583
|
+
* WHAT WROTE THIS MEASUREMENT.
|
|
1584
|
+
*
|
|
1585
|
+
* Two sites write probes for the same account, and before this field they
|
|
1586
|
+
* were indistinguishable in the file — which made a real confusion
|
|
1587
|
+
* unanswerable: the user presses "test", sees an account measured `ok`, and
|
|
1588
|
+
* later finds it `rate-limited`. Nothing in the record said whether that was
|
|
1589
|
+
* a NEWER measurement (a live request really did hit the limit) or an OLDER
|
|
1590
|
+
* one somehow winning, and the file keeps no history to tell them apart.
|
|
1591
|
+
*
|
|
1592
|
+
* With the source recorded, that question has an answer on sight: a
|
|
1593
|
+
* `live-request` entry that replaced a `test-batch` result is the pool
|
|
1594
|
+
* working as designed (a live failure steers the next request away from the
|
|
1595
|
+
* account); two entries of the SAME source with the OLDER one winning would
|
|
1596
|
+
* be a genuine bug.
|
|
1597
|
+
*
|
|
1598
|
+
* ABSENT for any measurement written before this field existed, so old probe
|
|
1599
|
+
* files stay readable — an absent source means "unknown", never a guess.
|
|
1600
|
+
*/
|
|
1601
|
+
source?: WorkBuddyPoolProbeSource;
|
|
1525
1602
|
/**
|
|
1526
1603
|
* When this account may be usable again, when the upstream stated a time.
|
|
1527
1604
|
*
|
|
@@ -1902,7 +1979,7 @@ type WorkBuddyWebUsage =
|
|
|
1902
1979
|
* here because this module is the node-free bridge and must not import the
|
|
1903
1980
|
* Host-side probe module (which pulls in the credential type).
|
|
1904
1981
|
*/
|
|
1905
|
-
type WorkBuddyWebProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1982
|
+
type WorkBuddyWebProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1906
1983
|
/** One model's probe result, as the card renders it. */
|
|
1907
1984
|
interface WorkBuddyWebProbeResult {
|
|
1908
1985
|
modelId: string;
|
|
@@ -1949,6 +2026,14 @@ interface WorkBuddyWebPoolAccount {
|
|
|
1949
2026
|
probe?: {
|
|
1950
2027
|
outcome: WorkBuddyWebProbeOutcome;
|
|
1951
2028
|
atMs: number;
|
|
2029
|
+
/**
|
|
2030
|
+
* What wrote this measurement, so the card can explain a change the user
|
|
2031
|
+
* watched happen: `test-batch` is a deliberate test, `live-request` is a
|
|
2032
|
+
* real request that failed and overwrote it. ABSENT for records written
|
|
2033
|
+
* before the field existed — the card must then say "unknown" rather than
|
|
2034
|
+
* guess which of the two it was.
|
|
2035
|
+
*/
|
|
2036
|
+
source?: 'test-batch' | 'live-request';
|
|
1952
2037
|
/** Only when the upstream named a time; never invented locally. */
|
|
1953
2038
|
retryAtMs?: number;
|
|
1954
2039
|
/**
|
|
@@ -2018,6 +2103,14 @@ interface WorkBuddyWebPool {
|
|
|
2018
2103
|
targetModelSource: 'preferred' | 'free' | 'none' | 'stale';
|
|
2019
2104
|
/** The account ids the user has checked into this region's pool. */
|
|
2020
2105
|
memberAccountIds?: readonly string[];
|
|
2106
|
+
/**
|
|
2107
|
+
* Tokens each test probe sends, as the user saved it; `0`/absent = default.
|
|
2108
|
+
*
|
|
2109
|
+
* The SAVED value, not the resolved one: the card needs to tell "the user
|
|
2110
|
+
* never chose a size" apart from "the user chose 25000", because the first
|
|
2111
|
+
* must keep showing the default option rather than a number they never picked.
|
|
2112
|
+
*/
|
|
2113
|
+
probeInputTokens?: number;
|
|
2021
2114
|
/**
|
|
2022
2115
|
* The subset of {@link memberAccountIds} that resolves to a real local
|
|
2023
2116
|
* sign-in — i.e. what a batch actually runs on.
|
|
@@ -2176,6 +2269,14 @@ interface WorkBuddyPoolDeps {
|
|
|
2176
2269
|
preferences(region: WorkBuddyRegion): {
|
|
2177
2270
|
enabled: boolean;
|
|
2178
2271
|
targetModelId: string;
|
|
2272
|
+
/**
|
|
2273
|
+
* Tokens each test probe sends as saved; `0` when the user never chose.
|
|
2274
|
+
*
|
|
2275
|
+
* Optional so a caller that has not learnt about the setting still
|
|
2276
|
+
* satisfies this interface, and so its absence reads as "default" — the
|
|
2277
|
+
* same meaning the host gives an older profile.
|
|
2278
|
+
*/
|
|
2279
|
+
probeInputTokens?: number;
|
|
2179
2280
|
memberAccountIds: readonly string[];
|
|
2180
2281
|
};
|
|
2181
2282
|
/** One region's CHECKED accounts, plus their credits and last measurements. */
|
|
@@ -2332,6 +2433,23 @@ interface WorkBuddyPoolPreferences {
|
|
|
2332
2433
|
enabled?: boolean;
|
|
2333
2434
|
/** Model id to test; `''` means auto-pick a free model from this region. */
|
|
2334
2435
|
targetModelId?: string;
|
|
2436
|
+
/**
|
|
2437
|
+
* How much input a probe sends, in tokens — the pool's biggest single knob.
|
|
2438
|
+
*
|
|
2439
|
+
* The upstream's rate limit (6004) fires on request SIZE, and the threshold
|
|
2440
|
+
* was measured to sit between 20k and 30k tokens. The probe has to be sized
|
|
2441
|
+
* near where the user's REAL conversations land: too small and it reports
|
|
2442
|
+
* "usable" while every long conversation is refused (the exact false positive
|
|
2443
|
+
* that made this knob necessary), too large and it costs real credit on every
|
|
2444
|
+
* batch AND excludes accounts that would have served the shorter requests the
|
|
2445
|
+
* user actually sends.
|
|
2446
|
+
*
|
|
2447
|
+
* A fixed value therefore cannot be right for everyone — the right size is a
|
|
2448
|
+
* property of the user's own traffic, which only they know. `undefined` keeps
|
|
2449
|
+
* the measured default ({@link DEFAULT_PROBE_INPUT_TOKENS}), so an existing
|
|
2450
|
+
* profile is unaffected.
|
|
2451
|
+
*/
|
|
2452
|
+
probeInputTokens?: number;
|
|
2335
2453
|
/**
|
|
2336
2454
|
* The account ids checked into this region's pool.
|
|
2337
2455
|
*
|
|
@@ -2486,4 +2604,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
|
|
|
2486
2604
|
*/
|
|
2487
2605
|
declare function apply(ctx: Context, config: Config): void;
|
|
2488
2606
|
//#endregion
|
|
2489
|
-
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID,
|
|
2607
|
+
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, WorkBuddyPoolPreferences, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|