dsh-workbuddy-connect 0.7.1 → 0.7.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/README.en.md +52 -14
- package/README.md +46 -14
- package/lib/bin.js +1 -1
- package/lib/client.js +253 -32
- package/lib/{host-heartbeat-L93zSVB5.js → host-heartbeat-DJb8l_4O.js} +328 -25
- package/lib/index.d.ts +73 -1
- package/lib/index.js +95 -7
- package/package.json +5 -3
|
@@ -846,6 +846,95 @@ function envelopeError(status, envelope) {
|
|
|
846
846
|
return /* @__PURE__ */ new Error(`workbuddy upstream ${kind} (http ${status}): ${envelope.msg.slice(0, 160)}`);
|
|
847
847
|
}
|
|
848
848
|
/**
|
|
849
|
+
* Parse the billing response's `CycleEndTime` into epoch milliseconds.
|
|
850
|
+
*
|
|
851
|
+
* The upstream sends a zone-less timestamp (`YYYY-MM-DD HH:mm:ss`). This reads it
|
|
852
|
+
* as the running machine's local time, which is what the official desktop client
|
|
853
|
+
* does — it feeds the same field straight to `new Date(value).getTime()`. That
|
|
854
|
+
* makes the plugin agree with the app on the same machine; it is **not** evidence
|
|
855
|
+
* of which zone the server or the account uses, which remains unestablished.
|
|
856
|
+
*
|
|
857
|
+
* A naive read does mean a machine in a different zone than the account's gets a
|
|
858
|
+
* shifted instant. That is accepted here rather than guessed at: inventing an
|
|
859
|
+
* offset would be asserting a zone nobody has verified.
|
|
860
|
+
*
|
|
861
|
+
* @returns Epoch milliseconds, or undefined when absent, malformed, or not a real
|
|
862
|
+
* calendar moment (see the round-trip check below).
|
|
863
|
+
*/
|
|
864
|
+
function parseCycleEndTime(value) {
|
|
865
|
+
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
866
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/u.exec(value.trim());
|
|
867
|
+
if (match === null) return void 0;
|
|
868
|
+
const [, y, mo, d, h, mi, s] = match;
|
|
869
|
+
const [year, month, day, hour, minute, second] = [
|
|
870
|
+
y,
|
|
871
|
+
mo,
|
|
872
|
+
d,
|
|
873
|
+
h,
|
|
874
|
+
mi,
|
|
875
|
+
s
|
|
876
|
+
].map(Number);
|
|
877
|
+
const date = new Date(year, month - 1, day, hour, minute, second);
|
|
878
|
+
const parsed = date.getTime();
|
|
879
|
+
if (!Number.isFinite(parsed)) return void 0;
|
|
880
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day || date.getHours() !== hour || date.getMinutes() !== minute || date.getSeconds() !== second) return;
|
|
881
|
+
return parsed;
|
|
882
|
+
}
|
|
883
|
+
/** Empty bucket, used whenever no account qualifies. */
|
|
884
|
+
const NO_EXPIRING_CREDIT = {
|
|
885
|
+
total: 0,
|
|
886
|
+
packages: 0
|
|
887
|
+
};
|
|
888
|
+
/**
|
|
889
|
+
* Sum the credit lapsing within `horizonMs` of `nowMs`.
|
|
890
|
+
*
|
|
891
|
+
* Only positive balances count: a package with nothing left is not a loss, and
|
|
892
|
+
* including it would make every long-lived account look permanently at risk.
|
|
893
|
+
* Only *future* expiries count too — a package already past its end time would
|
|
894
|
+
* otherwise satisfy every forward horizon at once (`end - now` is negative,
|
|
895
|
+
* hence "within 24 hours"), making a long-dead package look permanently
|
|
896
|
+
* imminent. See {@link WorkBuddyCreditExpiry} for why the past is not reported.
|
|
897
|
+
*/
|
|
898
|
+
function expiringWithin(accounts, nowMs, horizonMs) {
|
|
899
|
+
return sumExpiring(accounts, (end) => end > nowMs && end - nowMs <= horizonMs);
|
|
900
|
+
}
|
|
901
|
+
/** Shared accumulator over accounts whose end time passes `matches`. */
|
|
902
|
+
function sumExpiring(accounts, matches) {
|
|
903
|
+
let total = 0;
|
|
904
|
+
let packages = 0;
|
|
905
|
+
let earliestEndTimeMs;
|
|
906
|
+
for (const account of accounts) {
|
|
907
|
+
const end = account.endTimeMs;
|
|
908
|
+
if (end === void 0 || account.remain <= 0) continue;
|
|
909
|
+
if (!matches(end)) continue;
|
|
910
|
+
total += account.remain;
|
|
911
|
+
packages += 1;
|
|
912
|
+
if (earliestEndTimeMs === void 0 || end < earliestEndTimeMs) earliestEndTimeMs = end;
|
|
913
|
+
}
|
|
914
|
+
if (packages === 0) return NO_EXPIRING_CREDIT;
|
|
915
|
+
return {
|
|
916
|
+
total,
|
|
917
|
+
packages,
|
|
918
|
+
...earliestEndTimeMs === void 0 ? {} : { earliestEndTimeMs }
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Bucket an account's credit by how soon it lapses.
|
|
923
|
+
*
|
|
924
|
+
* Returns undefined when no account carried a usable end time, so a caller can
|
|
925
|
+
* tell "nothing is expiring" apart from "we never learned when anything
|
|
926
|
+
* expires" — the second must not render as reassurance.
|
|
927
|
+
*/
|
|
928
|
+
function summariseExpiry(accounts, nowMs) {
|
|
929
|
+
if (!accounts.some((account) => account.endTimeMs !== void 0)) return {};
|
|
930
|
+
const DAY = 864e5;
|
|
931
|
+
return { expiry: {
|
|
932
|
+
within24h: expiringWithin(accounts, nowMs, DAY),
|
|
933
|
+
within3d: expiringWithin(accounts, nowMs, 3 * DAY),
|
|
934
|
+
within7d: expiringWithin(accounts, nowMs, 7 * DAY)
|
|
935
|
+
} };
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
849
938
|
* Upstream HTTP client. One instance serves the whole plugin; requests take
|
|
850
939
|
* the credential explicitly so token refreshes apply on the next call.
|
|
851
940
|
*
|
|
@@ -887,7 +976,7 @@ var WorkBuddyUpstreamClient = class {
|
|
|
887
976
|
...chatHeaders(credential, userAgent, identity.clientVersion),
|
|
888
977
|
"Authorization": `Bearer ${credential.accessToken}`
|
|
889
978
|
},
|
|
890
|
-
body: region === "global" ? prepareInternationalChatBody(bodyJson) : bodyJson,
|
|
979
|
+
body: region === "global" ? prepareInternationalChatBody(bodyJson) : prepareCnChatBody(bodyJson),
|
|
891
980
|
...signal === void 0 ? {} : { signal }
|
|
892
981
|
});
|
|
893
982
|
} catch (error) {
|
|
@@ -1090,15 +1179,18 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1090
1179
|
else remain = capacityRemain;
|
|
1091
1180
|
if (remain < 0) remain = 0;
|
|
1092
1181
|
total += remain;
|
|
1182
|
+
const endTimeMs = parseCycleEndTime(account["CycleEndTime"]);
|
|
1093
1183
|
accounts.push({
|
|
1094
1184
|
packageName: typeof account["PackageName"] === "string" ? account["PackageName"] : "(unnamed)",
|
|
1095
1185
|
remain,
|
|
1096
|
-
size: size > 0 ? size : numberField("CapacitySize")
|
|
1186
|
+
size: size > 0 ? size : numberField("CapacitySize"),
|
|
1187
|
+
...endTimeMs === void 0 ? {} : { endTimeMs }
|
|
1097
1188
|
});
|
|
1098
1189
|
}
|
|
1099
1190
|
return {
|
|
1100
1191
|
total,
|
|
1101
|
-
accounts
|
|
1192
|
+
accounts,
|
|
1193
|
+
...summariseExpiry(accounts, now.getTime())
|
|
1102
1194
|
};
|
|
1103
1195
|
}
|
|
1104
1196
|
/**
|
|
@@ -1435,6 +1527,32 @@ function modelWithCurrentPromotion(model, now = Date.now()) {
|
|
|
1435
1527
|
* JSON object is returned unchanged, exactly as {@link prepareChatBody} does,
|
|
1436
1528
|
* so this is safe to run over an already-prepared-or-not body.
|
|
1437
1529
|
*/
|
|
1530
|
+
/**
|
|
1531
|
+
* Prepare a body for the **CN** endpoint.
|
|
1532
|
+
*
|
|
1533
|
+
* Until #87 the CN wire was the caller's body verbatim. The only difference
|
|
1534
|
+
* now is the shared {@link dropUnsupportedEffort} strip, whose region scope was
|
|
1535
|
+
* lifted once measurements showed this endpoint rejects `off` too.
|
|
1536
|
+
*
|
|
1537
|
+
* Deliberately **not** routed through {@link prepareChatBody}: the shim already
|
|
1538
|
+
* applies it before handing the body to {@link WorkBuddyUpstreamClient.chatStream},
|
|
1539
|
+
* and re-running it would be a second `stream`/`developer`/`tool_choice`
|
|
1540
|
+
* normalisation of an already-normalised body. Only the strip belongs here.
|
|
1541
|
+
*
|
|
1542
|
+
* A body that is not a JSON object is returned unchanged, so this is safe to
|
|
1543
|
+
* run over an already-prepared-or-not body.
|
|
1544
|
+
*/
|
|
1545
|
+
function prepareCnChatBody(source) {
|
|
1546
|
+
let body;
|
|
1547
|
+
try {
|
|
1548
|
+
body = JSON.parse(source);
|
|
1549
|
+
} catch {
|
|
1550
|
+
return source;
|
|
1551
|
+
}
|
|
1552
|
+
if (!isObject(body)) return source;
|
|
1553
|
+
dropUnsupportedEffort(body);
|
|
1554
|
+
return JSON.stringify(body);
|
|
1555
|
+
}
|
|
1438
1556
|
function prepareInternationalChatBody(source) {
|
|
1439
1557
|
const prepared = prepareChatBody(source);
|
|
1440
1558
|
let body;
|
|
@@ -1456,25 +1574,34 @@ function prepareInternationalChatBody(source) {
|
|
|
1456
1574
|
return JSON.stringify(body);
|
|
1457
1575
|
}
|
|
1458
1576
|
/**
|
|
1459
|
-
* Remove the adapter's own `off` effort spelling from the
|
|
1577
|
+
* Remove the adapter's own `off` effort spelling from the wire, on every region.
|
|
1460
1578
|
*
|
|
1461
1579
|
* `thinkingLevelMap.off` is pinned to the literal `'off'` for models that
|
|
1462
1580
|
* declare `canDisableThinking`, so that the level stays selectable. pi-ai
|
|
1463
|
-
* sends that value for any request carrying no explicit level
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1468
|
-
* `
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1581
|
+
* sends that value for any request carrying no explicit level — which is why
|
|
1582
|
+
* this covers **Default and Off alike**: the two produce the same body, so a
|
|
1583
|
+
* fix that only removed the menu entry would leave Default failing.
|
|
1584
|
+
*
|
|
1585
|
+
* Both endpoints reject it, which is why this is no longer region-scoped
|
|
1586
|
+
* (#87): the international GPT family answers HTTP 400 `11133` /
|
|
1587
|
+
* `extError.param === 'reasoning.effort'` (issue #49), and the CN endpoint
|
|
1588
|
+
* rejects it on models that declare `onlyReasoning` — measured on
|
|
1589
|
+
* `deepseek-v4.1-flash` with 400 / `11150`, while `low`/`high`/`max` pass
|
|
1590
|
+
* (issue #87). Omission is the only form measured good on every such model; a
|
|
1591
|
+
* literal `'none'` is *not* a safe substitute — accepted by the GPT-5.6 family
|
|
1592
|
+
* and GLM, rejected by `gpt-6-astra`.
|
|
1593
|
+
*
|
|
1594
|
+
* Consequences, stated honestly: the picker still offers Off for models where
|
|
1595
|
+
* the upstream declares `canDisableThinking`, but selecting it now means "the
|
|
1596
|
+
* field is omitted" — the model's actual behaviour is decided upstream and is
|
|
1597
|
+
* *not* guaranteed to disable thinking or to match the catalog's
|
|
1598
|
+
* `defaultEffort`. Declared spellings (`low`/`medium`/`high`/`xhigh`/`max`)
|
|
1599
|
+
* and an explicit `none` pass through untouched.
|
|
1600
|
+
*
|
|
1601
|
+
* The CN strip replaces a scope decision whose premise did not hold: the
|
|
1602
|
+
* earlier note here said that endpoint "has accepted this spelling in every
|
|
1603
|
+
* measurement so far". Measurements since (#87) show it rejects `off` on
|
|
1604
|
+
* validating models — the earlier runs had simply not hit one.
|
|
1478
1605
|
*/
|
|
1479
1606
|
function dropUnsupportedEffort(obj) {
|
|
1480
1607
|
if (obj["reasoning_effort"] === "off") delete obj["reasoning_effort"];
|
|
@@ -1874,6 +2001,60 @@ const WINDOWS_REGISTRY_ROOTS = [
|
|
|
1874
2001
|
];
|
|
1875
2002
|
const WINDOWS_REGISTRY_OUTPUT_MAX_BYTES = 1048576;
|
|
1876
2003
|
const WINDOWS_ELECTRON_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
|
|
2004
|
+
/**
|
|
2005
|
+
* The PowerShell command that re-reads one uninstall root as UTF-8.
|
|
2006
|
+
*
|
|
2007
|
+
* Used *only* as a fallback, and only to supply data: the caller feeds its
|
|
2008
|
+
* output through the same {@link parseWindowsRegistryOutput} and
|
|
2009
|
+
* {@link inspectWindowsElectronCandidate} as the `reg.exe` path, so candidate
|
|
2010
|
+
* validation, uniqueness, and the "we could not check" rules are unchanged.
|
|
2011
|
+
*
|
|
2012
|
+
* Why a different tool rather than a smarter decode: `reg.exe` writes in a
|
|
2013
|
+
* code page chosen by the console it inherits, and no inspection of the
|
|
2014
|
+
* resulting bytes can recover which one. .NET's registry API returns strings,
|
|
2015
|
+
* so the question never arises.
|
|
2016
|
+
*
|
|
2017
|
+
* Structure notes, each one a defect fixed after review:
|
|
2018
|
+
*
|
|
2019
|
+
* - **`param` is the first statement.** A `param` block must lead the script
|
|
2020
|
+
* block; putting the encoding assignment before it is a parse error in
|
|
2021
|
+
* Windows PowerShell 5.1.
|
|
2022
|
+
* - **`[Console]::OutputEncoding` is set explicitly.** Windows PowerShell 5.1
|
|
2023
|
+
* encodes redirected stdout with it, so UTF-8 is *defined* for this child
|
|
2024
|
+
* rather than inferred from what comes back.
|
|
2025
|
+
* - **Exit codes distinguish "root absent" from "root unreadable"** (`2` and
|
|
2026
|
+
* `3`). The caller branches on the code, never on message text, so the
|
|
2027
|
+
* localized diagnostics below cannot change an outcome.
|
|
2028
|
+
* - **An unopenable subkey is `3`, not `continue`.** Skipping it silently
|
|
2029
|
+
* could drop a real candidate and make the product look absent, which is
|
|
2030
|
+
* precisely the confusion the caller's `incomplete` state exists to prevent.
|
|
2031
|
+
* - **Output is JSON.** PowerShell does the escaping, so paths containing
|
|
2032
|
+
* spaces, commas, or quotes no longer depend on column positions, and only
|
|
2033
|
+
* the two values this plugin reads are collected.
|
|
2034
|
+
*
|
|
2035
|
+
* The root is passed as a `-Root` argument, never interpolated into the
|
|
2036
|
+
* command text.
|
|
2037
|
+
*/
|
|
2038
|
+
const WINDOWS_REGISTRY_RELATIVE_POWERSHELL = "System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
2039
|
+
/** Exit code meaning the root key does not exist (a normal, expected outcome). */
|
|
2040
|
+
const WINDOWS_POWERSHELL_ROOT_ABSENT = 2;
|
|
2041
|
+
/** Exit code meaning the root exists but a part of it could not be read. */
|
|
2042
|
+
const WINDOWS_POWERSHELL_ROOT_UNREADABLE = 3;
|
|
2043
|
+
const WINDOWS_POWERSHELL_QUERY_SCRIPT = [
|
|
2044
|
+
"param([string]$Root)",
|
|
2045
|
+
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
|
|
2046
|
+
"$ErrorActionPreference=\"Stop\";",
|
|
2047
|
+
"$path=\"Registry::\"+$Root;",
|
|
2048
|
+
"if (-not (Test-Path -LiteralPath $path)) { [Console]::Error.Write(\"root absent\"); exit 2 };",
|
|
2049
|
+
"try { $k=Get-Item -LiteralPath $path -ErrorAction Stop } catch { [Console]::Error.Write(\"root unreadable\"); exit 3 };",
|
|
2050
|
+
"$out=@();",
|
|
2051
|
+
"foreach ($n in $k.GetSubKeyNames()) {",
|
|
2052
|
+
" try { $p=$k.OpenSubKey($n); if ($null -eq $p) { throw \"null\" } } catch { [Console]::Error.Write(\"subkey unreadable\"); exit 3 };",
|
|
2053
|
+
" $d=$p.GetValue(\"DisplayName\");$i=$p.GetValue(\"DisplayIcon\");",
|
|
2054
|
+
" if ($null -ne $d -or $null -ne $i) { $out += [pscustomobject]@{ displayName=[string]$d; displayIcon=[string]$i } }",
|
|
2055
|
+
"};",
|
|
2056
|
+
"ConvertTo-Json -InputObject @($out) -Compress -Depth 3"
|
|
2057
|
+
].join(" ");
|
|
1877
2058
|
/** One discovery subprocess's own limits; see {@link WorkBuddyAtRestKeyProviderOptions}. */
|
|
1878
2059
|
const WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS = 3e3;
|
|
1879
2060
|
const MDFIND_MAX_OUTPUT_BYTES = 1048576;
|
|
@@ -1968,6 +2149,15 @@ function workBuddyDiscoveryTools(bundleId) {
|
|
|
1968
2149
|
function workBuddyWindowsDiscoveryTools() {
|
|
1969
2150
|
const systemRoot = process.env.SystemRoot?.trim();
|
|
1970
2151
|
const regPath = systemRoot === void 0 || systemRoot === "" ? void 0 : join(systemRoot, "System32", "reg.exe");
|
|
2152
|
+
/**
|
|
2153
|
+
* The fallback interpreter. Resolved by absolute path from SystemRoot, like
|
|
2154
|
+
* `reg.exe`: never through PATH, which a user can change.
|
|
2155
|
+
*
|
|
2156
|
+
* Windows PowerShell 5.1 ships with Windows, so unlike `pwsh` it can be
|
|
2157
|
+
* relied on; the script is written to its syntax (see
|
|
2158
|
+
* {@link WINDOWS_POWERSHELL_QUERY_SCRIPT}).
|
|
2159
|
+
*/
|
|
2160
|
+
const powershellPath = systemRoot === void 0 || systemRoot === "" ? void 0 : join(systemRoot, WINDOWS_REGISTRY_RELATIVE_POWERSHELL);
|
|
1971
2161
|
const runTool = (root, signal) => new Promise((resolve, reject) => {
|
|
1972
2162
|
if (regPath === void 0) {
|
|
1973
2163
|
reject(new DiscoveryIncompleteError("SystemRoot is not configured"));
|
|
@@ -1985,13 +2175,14 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
1985
2175
|
], {
|
|
1986
2176
|
maxBuffer: WINDOWS_REGISTRY_OUTPUT_MAX_BYTES,
|
|
1987
2177
|
timeout: WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS,
|
|
1988
|
-
windowsHide: true
|
|
2178
|
+
windowsHide: true,
|
|
2179
|
+
encoding: "buffer"
|
|
1989
2180
|
}, (error, stdout, stderr) => {
|
|
1990
2181
|
if (settled) return;
|
|
1991
2182
|
settled = true;
|
|
1992
2183
|
if (error !== null && error !== void 0) {
|
|
1993
|
-
if (error.killed !== true && (error.code === 1 || error.code === "1") && windowsRegistryKeyMissing(stderr)) {
|
|
1994
|
-
resolve(
|
|
2184
|
+
if (error.killed !== true && (error.code === 1 || error.code === "1") && windowsRegistryKeyMissing(stderr.toString("utf8"))) {
|
|
2185
|
+
resolve(Buffer.alloc(0));
|
|
1995
2186
|
return;
|
|
1996
2187
|
}
|
|
1997
2188
|
reject(new DiscoveryIncompleteError(`reg.exe could not complete (${error.killed === true ? "timed out" : String(error.code ?? "unavailable")})`));
|
|
@@ -2010,7 +2201,119 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
2010
2201
|
signal.removeEventListener("abort", abort);
|
|
2011
2202
|
});
|
|
2012
2203
|
});
|
|
2013
|
-
|
|
2204
|
+
/**
|
|
2205
|
+
* Re-read one root through PowerShell's Unicode registry API (#88).
|
|
2206
|
+
*
|
|
2207
|
+
* Returns the same column-shaped document the `reg.exe` path produces, so
|
|
2208
|
+
* the caller's parser and candidate checks apply unchanged. Every failure
|
|
2209
|
+
* that means "we could not tell" is a {@link DiscoveryIncompleteError}, and
|
|
2210
|
+
* a root that simply does not exist yields an empty document — one of the
|
|
2211
|
+
* three roots being absent is normal and must not fail the search.
|
|
2212
|
+
*/
|
|
2213
|
+
const runFallback = (root, signal) => new Promise((resolve, reject) => {
|
|
2214
|
+
if (powershellPath === void 0) {
|
|
2215
|
+
reject(new DiscoveryIncompleteError("SystemRoot is not configured"));
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (signal.aborted) {
|
|
2219
|
+
reject(new DiscoveryIncompleteError("powershell.exe was not started: the discovery budget was already spent"));
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
let settled = false;
|
|
2223
|
+
const child = execFile(powershellPath, [
|
|
2224
|
+
"-NoProfile",
|
|
2225
|
+
"-NonInteractive",
|
|
2226
|
+
"-Command",
|
|
2227
|
+
WINDOWS_POWERSHELL_QUERY_SCRIPT,
|
|
2228
|
+
"-Root",
|
|
2229
|
+
root
|
|
2230
|
+
], {
|
|
2231
|
+
maxBuffer: WINDOWS_REGISTRY_OUTPUT_MAX_BYTES,
|
|
2232
|
+
timeout: WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS,
|
|
2233
|
+
windowsHide: true,
|
|
2234
|
+
encoding: "utf8"
|
|
2235
|
+
}, (error, stdout) => {
|
|
2236
|
+
if (settled) return;
|
|
2237
|
+
settled = true;
|
|
2238
|
+
if (error !== null && error !== void 0) {
|
|
2239
|
+
const code = error.killed === true ? void 0 : Number(error.code);
|
|
2240
|
+
if (code === WINDOWS_POWERSHELL_ROOT_ABSENT) {
|
|
2241
|
+
resolve("");
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
if (code === WINDOWS_POWERSHELL_ROOT_UNREADABLE) {
|
|
2245
|
+
reject(new DiscoveryIncompleteError(`powershell.exe could not read the registry root ${root}`));
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
reject(new DiscoveryIncompleteError(`powershell.exe could not complete (${error.killed === true ? "timed out" : String(error.code ?? "unavailable")})`));
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
const records = parseWindowsUninstallJson(stdout);
|
|
2252
|
+
if (records === void 0) {
|
|
2253
|
+
reject(new DiscoveryIncompleteError("powershell.exe output was not the expected registry document"));
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
resolve(records);
|
|
2257
|
+
});
|
|
2258
|
+
const abort = () => {
|
|
2259
|
+
if (settled) return;
|
|
2260
|
+
settled = true;
|
|
2261
|
+
child.kill();
|
|
2262
|
+
reject(new DiscoveryIncompleteError("powershell.exe was abandoned: the discovery budget was spent"));
|
|
2263
|
+
};
|
|
2264
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
2265
|
+
child.on("close", () => {
|
|
2266
|
+
signal.removeEventListener("abort", abort);
|
|
2267
|
+
});
|
|
2268
|
+
});
|
|
2269
|
+
return { queryUninstallRoot: async (root, signal) => {
|
|
2270
|
+
const raw = await runTool(root, signal);
|
|
2271
|
+
if (isAsciiOnly(raw)) return raw.toString("utf8");
|
|
2272
|
+
return await runFallback(root, signal);
|
|
2273
|
+
} };
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Whether every byte is ASCII, the one case where a decode cannot be wrong.
|
|
2277
|
+
*
|
|
2278
|
+
* @param bytes - the child's raw stdout.
|
|
2279
|
+
*/
|
|
2280
|
+
function isAsciiOnly(bytes) {
|
|
2281
|
+
for (const byte of bytes) if (byte >= 128) return false;
|
|
2282
|
+
return true;
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Turn the fallback's JSON into the same column-shaped document
|
|
2286
|
+
* {@link parseWindowsRegistryOutput} already consumes.
|
|
2287
|
+
*
|
|
2288
|
+
* Re-encoding into the `reg query` text form on purpose: the parser and
|
|
2289
|
+
* {@link parseWindowsDisplayIcon} carry the candidate-quoting rules that are
|
|
2290
|
+
* shared with existing tests, and the seam between this tool and the provider
|
|
2291
|
+
* is a string. Records carrying neither value are skipped, and the key name is
|
|
2292
|
+
* synthetic because only the values are read.
|
|
2293
|
+
*
|
|
2294
|
+
* @param output - the child's UTF-8 stdout.
|
|
2295
|
+
* @returns the document, or `undefined` when it was not the expected JSON.
|
|
2296
|
+
*/
|
|
2297
|
+
function parseWindowsUninstallJson(output) {
|
|
2298
|
+
let document;
|
|
2299
|
+
try {
|
|
2300
|
+
document = JSON.parse(output);
|
|
2301
|
+
} catch {
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
const rows = Array.isArray(document) ? document : [document];
|
|
2305
|
+
const lines = [];
|
|
2306
|
+
for (const [index, row] of rows.entries()) {
|
|
2307
|
+
if (typeof row !== "object" || row === null) continue;
|
|
2308
|
+
const record = row;
|
|
2309
|
+
const displayName = record["displayName"];
|
|
2310
|
+
const displayIcon = record["displayIcon"];
|
|
2311
|
+
if (typeof displayName !== "string" && typeof displayIcon !== "string") continue;
|
|
2312
|
+
lines.push(`HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{PS-${String(index)}}`);
|
|
2313
|
+
if (typeof displayName === "string") lines.push(` DisplayName REG_SZ ${displayName}`);
|
|
2314
|
+
if (typeof displayIcon === "string") lines.push(` DisplayIcon REG_SZ ${displayIcon}`);
|
|
2315
|
+
}
|
|
2316
|
+
return lines.join("\r\n");
|
|
2014
2317
|
}
|
|
2015
2318
|
/**
|
|
2016
2319
|
* In-memory protector-key resolver: one spawn per key id, single-flight, never
|
|
@@ -3635,7 +3938,7 @@ var WorkBuddyCatalog = class {
|
|
|
3635
3938
|
};
|
|
3636
3939
|
//#endregion
|
|
3637
3940
|
//#region src/version.ts
|
|
3638
|
-
const WORKBUDDY_CONNECT_VERSION = "0.7.
|
|
3941
|
+
const WORKBUDDY_CONNECT_VERSION = "0.7.2";
|
|
3639
3942
|
//#endregion
|
|
3640
3943
|
//#region src/host-heartbeat.ts
|
|
3641
3944
|
/**
|
|
@@ -3806,4 +4109,4 @@ function isHeartbeatProcessAlive(heartbeat) {
|
|
|
3806
4109
|
return startAtMs <= heartbeat.registeredAt;
|
|
3807
4110
|
}
|
|
3808
4111
|
//#endregion
|
|
3809
|
-
export { extractDisplayErrorMessage as A,
|
|
4112
|
+
export { validAppVersion as $, extractDisplayErrorMessage as A, randomSentinel as B, WORKBUDDY_VARIANTS as C, WORKBUDDY_UPDATE_PATH as D, WORKBUDDY_STATUS_PATH as E, prepareCnChatBody as F, readCliVersion as G, FALLBACK_CN_APP_VERSION as H, prepareInternationalChatBody as I, WORKBUDDY_APP_VERSION_FILENAME as J, resolveChatIdentity as K, regionOf as L, normalizeCredits as M, parseModelCatalog as N, WorkBuddyUpstreamClient as O, prepareChatBody as P, resolveAppVersion as Q, PROBE_EFFORT_CANDIDATES as R, CN_VARIANT as S, WORKBUDDY_PROBE_PATH as T, chatUserAgent as U, CN_APP_VERSION_FILENAME as V, fallbackChatIdentity as W, installedAppVersion as X, appUserAgent as Y, readBundleVersion as Z, desktopAuthCandidatesFor as _, readHostHeartbeat as a, atRestKeyProviderFor as b, WORKBUDDY_CONNECT_VERSION as c, WorkBuddyCatalog as d, WORKBUDDY_AUTH_FILENAME as f, defaultDesktopAuthPath as g, defaultDesktopAuthCandidates as h, processStartTimeMs as i, modelWithCurrentPromotion as j, classifyUpstreamError as k, FALLBACK_WORKBUDDY_AI_MODELS as l, WorkBuddyCredentialStore as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, WORKBUDDY_AUTH_FILE_ENV as p, validCliVersion as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS as u, parseWorkBuddyAuth as v, variantFor as w, AI_VARIANT as x, workbuddyOwnAuthPath as y, probeModel as z };
|
package/lib/index.d.ts
CHANGED
|
@@ -330,6 +330,49 @@ interface WorkBuddyCreditAccount {
|
|
|
330
330
|
remain: number;
|
|
331
331
|
size: number;
|
|
332
332
|
unlimited?: true;
|
|
333
|
+
/**
|
|
334
|
+
* When the package's own cycle ends, in epoch milliseconds.
|
|
335
|
+
*
|
|
336
|
+
* The upstream has always sent this (`CycleEndTime`, a zone-less
|
|
337
|
+
* `YYYY-MM-DD HH:mm:ss` string) and the query itself already filters on
|
|
338
|
+
* `PackageEndTimeRange`, but the field used to be dropped here — so a card
|
|
339
|
+
* could show "500 credit" for a package expiring in an hour with nothing to
|
|
340
|
+
* distinguish it from one expiring next month. Absent when the upstream omits
|
|
341
|
+
* or malforms it, which must never read as "no expiry".
|
|
342
|
+
*
|
|
343
|
+
* The zone the upstream means is **not established**: reading the string as
|
|
344
|
+
* the running machine's local time follows the official desktop client (see
|
|
345
|
+
* {@link parseCycleEndTime}), and is not a claim about the account's zone.
|
|
346
|
+
*/
|
|
347
|
+
endTimeMs?: number;
|
|
348
|
+
}
|
|
349
|
+
/** Credit whose package expires within a horizon the user should act on. */
|
|
350
|
+
interface WorkBuddyExpiringCredits {
|
|
351
|
+
/** Sum of {@link WorkBuddyCreditAccount.remain} over the window. */
|
|
352
|
+
total: number;
|
|
353
|
+
/** How many packages contribute to {@link total}. */
|
|
354
|
+
packages: number;
|
|
355
|
+
/** The soonest `endTimeMs` among them. */
|
|
356
|
+
earliestEndTimeMs?: number;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Credit at risk, bucketed by how soon it lapses.
|
|
360
|
+
*
|
|
361
|
+
* Thresholds are calendar-ish (24h / 3d / 7d) rather than "this month": a package
|
|
362
|
+
* expiring at 08:00 tomorrow is not helped by a monthly average, and the upstream
|
|
363
|
+
* genuinely staggers expiries per package.
|
|
364
|
+
*
|
|
365
|
+
* Only *future* horizons are reported. An already-lapsed package is deliberately
|
|
366
|
+
* **not** summarised here: the personal billing query this feeds filters
|
|
367
|
+
* `Status: [0, 3]` (`valid` + `usedUp`) from the current time forward, while the
|
|
368
|
+
* upstream marks a lapsed package `Status: 2` (`expired`) — a status this query
|
|
369
|
+
* never returns. Historical expiry would need its own query, which is out of
|
|
370
|
+
* scope.
|
|
371
|
+
*/
|
|
372
|
+
interface WorkBuddyCreditExpiry {
|
|
373
|
+
within24h: WorkBuddyExpiringCredits;
|
|
374
|
+
within3d: WorkBuddyExpiringCredits;
|
|
375
|
+
within7d: WorkBuddyExpiringCredits;
|
|
333
376
|
}
|
|
334
377
|
/** Aggregated credit answer for one credential. */
|
|
335
378
|
interface WorkBuddyCredits {
|
|
@@ -347,6 +390,13 @@ interface WorkBuddyCredits {
|
|
|
347
390
|
*/
|
|
348
391
|
unlimited?: true;
|
|
349
392
|
cycleResetTime?: string;
|
|
393
|
+
/**
|
|
394
|
+
* Credit lapsing soon, so a card can warn instead of only reporting a total.
|
|
395
|
+
*
|
|
396
|
+
* Absent when no account carried a usable end time — the plugin must not
|
|
397
|
+
* claim "nothing expires soon" on the strength of data it never received.
|
|
398
|
+
*/
|
|
399
|
+
expiry?: WorkBuddyCreditExpiry;
|
|
350
400
|
}
|
|
351
401
|
/** Token refresh answer; fields the upstream omits stay absent. */
|
|
352
402
|
interface WorkBuddyRefreshOutcome {
|
|
@@ -609,6 +659,22 @@ declare function modelWithCurrentPromotion(model: WorkBuddyUpstreamModel, now?:
|
|
|
609
659
|
* JSON object is returned unchanged, exactly as {@link prepareChatBody} does,
|
|
610
660
|
* so this is safe to run over an already-prepared-or-not body.
|
|
611
661
|
*/
|
|
662
|
+
/**
|
|
663
|
+
* Prepare a body for the **CN** endpoint.
|
|
664
|
+
*
|
|
665
|
+
* Until #87 the CN wire was the caller's body verbatim. The only difference
|
|
666
|
+
* now is the shared {@link dropUnsupportedEffort} strip, whose region scope was
|
|
667
|
+
* lifted once measurements showed this endpoint rejects `off` too.
|
|
668
|
+
*
|
|
669
|
+
* Deliberately **not** routed through {@link prepareChatBody}: the shim already
|
|
670
|
+
* applies it before handing the body to {@link WorkBuddyUpstreamClient.chatStream},
|
|
671
|
+
* and re-running it would be a second `stream`/`developer`/`tool_choice`
|
|
672
|
+
* normalisation of an already-normalised body. Only the strip belongs here.
|
|
673
|
+
*
|
|
674
|
+
* A body that is not a JSON object is returned unchanged, so this is safe to
|
|
675
|
+
* run over an already-prepared-or-not body.
|
|
676
|
+
*/
|
|
677
|
+
declare function prepareCnChatBody(source: string): string;
|
|
612
678
|
declare function prepareInternationalChatBody(source: string): string;
|
|
613
679
|
//#endregion
|
|
614
680
|
//#region src/variants.d.ts
|
|
@@ -1276,6 +1342,12 @@ declare const WORKBUDDY_STREAM_IDLE_TIMEOUT_MS = 300000;
|
|
|
1276
1342
|
interface WorkBuddyAdapterOptions {
|
|
1277
1343
|
providerId?: string;
|
|
1278
1344
|
displayName?: string;
|
|
1345
|
+
/**
|
|
1346
|
+
* Which endpoint this adapter serves, from the variant descriptor. Decides
|
|
1347
|
+
* only whether the CN `onlyReasoning` veto hides `off` (#87); defaults to
|
|
1348
|
+
* `'cn'`, the same legacy default `providerId` carries.
|
|
1349
|
+
*/
|
|
1350
|
+
region?: WorkBuddyRegion;
|
|
1279
1351
|
shim: WorkBuddyShim;
|
|
1280
1352
|
store: WorkBuddyCredentialStore;
|
|
1281
1353
|
catalog: WorkBuddyCatalog;
|
|
@@ -1645,4 +1717,4 @@ declare function visibilityAccountOf(credential: Pick<WorkBuddyCredential, 'uid'
|
|
|
1645
1717
|
*/
|
|
1646
1718
|
declare function apply(ctx: Context, config: Config): void;
|
|
1647
1719
|
//#endregion
|
|
1648
|
-
export { AI_VARIANT, type AppVersionInfo, CN_APP_VERSION_FILENAME, CN_VARIANT, type ChatIdentity, Config, FALLBACK_CN_APP_VERSION, FALLBACK_WORKBUDDY_AI_MODELS, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, type ResolveChatIdentityOptions, type UpstreamErrorKind, WORKBUDDY_AI_SETTINGS_NS, WORKBUDDY_APP_VERSION_FILENAME, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CATALOG_FILENAME, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_VARIANTS, WORKBUDDY_VISIBILITY_FILENAME, type WorkBuddyAdapter, type WorkBuddyAppVersionSource, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyCatalogFetch, WorkBuddyCatalogStore, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyProbeRecord, WorkBuddyProbeService, type WorkBuddyProbeStatus, WorkBuddyProbeStore, type WorkBuddyProbeValidation, type WorkBuddyPromotion, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyVariant, WorkBuddyVisibilityStore, appUserAgent, apply, chatUserAgent, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, desktopAuthCandidatesFor, fallbackChatIdentity, fingerprintModel, inject, installedAppVersion, isHeartbeatProcessAlive, modelWithCurrentPromotion, name, normalizeCredits, parseModelCatalog, parseWorkBuddyAuth, prepareChatBody, prepareInternationalChatBody, probeModel, processStartTimeMs, randomSentinel, readBundleVersion, readCliVersion, readHostHeartbeat, regionOf, resolveAppVersion, resolveChatIdentity, validAppVersion, validCliVersion, variantFor, visibilityAccountOf, workbuddyCatalogPath, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath, workbuddyVisibilityPath };
|
|
1720
|
+
export { AI_VARIANT, type AppVersionInfo, CN_APP_VERSION_FILENAME, CN_VARIANT, type ChatIdentity, Config, FALLBACK_CN_APP_VERSION, FALLBACK_WORKBUDDY_AI_MODELS, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, type ResolveChatIdentityOptions, type UpstreamErrorKind, WORKBUDDY_AI_SETTINGS_NS, WORKBUDDY_APP_VERSION_FILENAME, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CATALOG_FILENAME, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_VARIANTS, WORKBUDDY_VISIBILITY_FILENAME, type WorkBuddyAdapter, type WorkBuddyAppVersionSource, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyCatalogFetch, WorkBuddyCatalogStore, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyProbeRecord, WorkBuddyProbeService, type WorkBuddyProbeStatus, WorkBuddyProbeStore, type WorkBuddyProbeValidation, type WorkBuddyPromotion, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyVariant, WorkBuddyVisibilityStore, appUserAgent, apply, chatUserAgent, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, desktopAuthCandidatesFor, fallbackChatIdentity, fingerprintModel, inject, installedAppVersion, isHeartbeatProcessAlive, modelWithCurrentPromotion, name, normalizeCredits, parseModelCatalog, parseWorkBuddyAuth, prepareChatBody, prepareCnChatBody, prepareInternationalChatBody, probeModel, processStartTimeMs, randomSentinel, readBundleVersion, readCliVersion, readHostHeartbeat, regionOf, resolveAppVersion, resolveChatIdentity, validAppVersion, validCliVersion, variantFor, visibilityAccountOf, workbuddyCatalogPath, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath, workbuddyVisibilityPath };
|