@fitchmultz/pi-mcp-adapter 8.0.6 → 8.0.7

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +128 -8
  2. package/OAUTH.md +13 -16
  3. package/README.md +8 -8
  4. package/cli.js +67 -28
  5. package/dist/abort.js +18 -9
  6. package/dist/client-operations.js +4 -2
  7. package/dist/commands.js +77 -43
  8. package/dist/config.js +194 -105
  9. package/dist/connection-runtime.d.ts +2 -2
  10. package/dist/connection-runtime.js +29 -18
  11. package/dist/consent-manager.d.ts +2 -2
  12. package/dist/consent-manager.js +5 -2
  13. package/dist/direct-tools.d.ts +1 -1
  14. package/dist/direct-tools.js +13 -7
  15. package/dist/elicitation-handler.d.ts +2 -3
  16. package/dist/elicitation-handler.js +113 -58
  17. package/dist/gateway-arguments.d.ts +1 -1
  18. package/dist/gateway-arguments.js +90 -23
  19. package/dist/glimpse-ui.js +10 -5
  20. package/dist/host-html-template.d.ts +3 -1
  21. package/dist/host-html-template.js +9 -12
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +264 -119
  24. package/dist/init.d.ts +1 -3
  25. package/dist/init.js +91 -51
  26. package/dist/legacy-migration.d.ts +1 -1
  27. package/dist/legacy-migration.js +17 -12
  28. package/dist/lifecycle.d.ts +3 -3
  29. package/dist/lifecycle.js +33 -17
  30. package/dist/logger.d.ts +2 -2
  31. package/dist/logger.js +4 -2
  32. package/dist/mcp-auth-flow.js +150 -58
  33. package/dist/mcp-auth.d.ts +7 -7
  34. package/dist/mcp-auth.js +189 -138
  35. package/dist/mcp-callback-server.js +19 -8
  36. package/dist/mcp-code.js +168 -54
  37. package/dist/mcp-keyring-helper.cjs +57 -23
  38. package/dist/mcp-oauth-provider.d.ts +6 -7
  39. package/dist/mcp-oauth-provider.js +146 -72
  40. package/dist/mcp-oauth-runtime.js +29 -16
  41. package/dist/mcp-output-guard.js +193 -65
  42. package/dist/mcp-output-preview.js +41 -20
  43. package/dist/mcp-panel.d.ts +9 -9
  44. package/dist/mcp-panel.js +141 -70
  45. package/dist/mcp-script-worker.mjs +135 -43
  46. package/dist/mcp-setup-panel.d.ts +8 -8
  47. package/dist/mcp-setup-panel.js +130 -44
  48. package/dist/mcp-status.js +14 -7
  49. package/dist/mcp-trace.d.ts +10 -5
  50. package/dist/mcp-trace.js +127 -61
  51. package/dist/metadata-cache.d.ts +3 -4
  52. package/dist/metadata-cache.js +83 -43
  53. package/dist/npx-resolver.js +101 -60
  54. package/dist/onboarding-state.js +4 -2
  55. package/dist/plain-result-reader.js +78 -31
  56. package/dist/prompts.js +36 -20
  57. package/dist/proxy-modes.js +670 -193
  58. package/dist/resource-tools.js +30 -9
  59. package/dist/runtime-owner.js +36 -18
  60. package/dist/sampling-handler.d.ts +1 -2
  61. package/dist/sampling-handler.js +41 -21
  62. package/dist/search-ranking.d.ts +1 -1
  63. package/dist/search-ranking.js +67 -34
  64. package/dist/server-manager.d.ts +11 -11
  65. package/dist/server-manager.js +283 -166
  66. package/dist/session-recovery.js +113 -61
  67. package/dist/tool-approval.js +9 -3
  68. package/dist/tool-loader.js +154 -55
  69. package/dist/tool-metadata.d.ts +1 -1
  70. package/dist/tool-metadata.js +51 -23
  71. package/dist/tool-registrar.js +32 -10
  72. package/dist/tool-result-renderer.js +58 -33
  73. package/dist/types.d.ts +2 -2
  74. package/dist/types.js +39 -18
  75. package/dist/ui-message-history.js +33 -13
  76. package/dist/ui-resource-handler.d.ts +3 -3
  77. package/dist/ui-resource-handler.js +37 -21
  78. package/dist/ui-server.js +101 -43
  79. package/dist/ui-session.js +66 -29
  80. package/dist/ui-stream-types.js +14 -4
  81. package/dist/ui-tool-visibility.js +12 -6
  82. package/dist/unix-socket-transport.d.ts +1 -2
  83. package/dist/unix-socket-transport.js +19 -12
  84. package/dist/utils.d.ts +2 -0
  85. package/dist/utils.js +60 -35
  86. package/package.json +26 -2
  87. package/skills/mcp-scripting/SKILL.md +6 -6
package/OAUTH.md CHANGED
@@ -42,6 +42,7 @@ For most MCP servers, you only need the URL:
42
42
  ```
43
43
 
44
44
  OAuth is automatically enabled for HTTP servers. The SDK will:
45
+
45
46
  - Auto-detect if the server requires OAuth
46
47
  - Discover OAuth endpoints from the server
47
48
  - Reuse a configured/stored client, use CIMD when supported, or register dynamically
@@ -69,8 +70,6 @@ You can optionally provide a pre-registered client:
69
70
  }
70
71
  ```
71
72
 
72
-
73
-
74
73
  ### Configuration Options
75
74
 
76
75
  - `url` - The MCP server URL (required)
@@ -145,13 +144,13 @@ Set `oauth.privateKeyJwt` to authenticate native token requests with `private_ke
145
144
  }
146
145
  ```
147
146
 
148
- | Option | Meaning |
149
- | --- | --- |
150
- | `privateKey` | PKCS#8 PEM string, JWK object or JWK JSON string. Strings support `${VAR}`, `$env:VAR`, `{env:VAR}` and existing `!command` / `!!` conventions, resolved only when authenticating. |
151
- | `algorithm` | Required asymmetric JOSE algorithm. Native PEM import supports RS, PS and ES families; EdDSA / Ed25519 require a JWK. The SDK validates supported key/algorithm combinations. HMAC (`HS*`) and `none` are not private-key authentication. |
152
- | `audience` | Optional nonempty JWT audience string; defaults to the discovered authorization-server issuer, then token URL. It does not change the request destination. |
153
- | `lifetimeSeconds` | Optional positive integer; native default is 300 seconds. |
154
- | `claims` | Optional object of additional JWT claims. Native `iss`, `sub`, `aud`, `iat`, `exp` and `jti` take precedence over overlapping entries. |
147
+ | Option | Meaning |
148
+ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
149
+ | `privateKey` | PKCS#8 PEM string, JWK object or JWK JSON string. Strings support `${VAR}`, `$env:VAR`, `{env:VAR}` and existing `!command` / `!!` conventions, resolved only when authenticating. |
150
+ | `algorithm` | Required asymmetric JOSE algorithm. Native PEM import supports RS, PS and ES families; EdDSA / Ed25519 require a JWK. The SDK validates supported key/algorithm combinations. HMAC (`HS*`) and `none` are not private-key authentication. |
151
+ | `audience` | Optional nonempty JWT audience string; defaults to the discovered authorization-server issuer, then token URL. It does not change the request destination. |
152
+ | `lifetimeSeconds` | Optional positive integer; native default is 300 seconds. |
153
+ | `claims` | Optional object of additional JWT claims. Native `iss`, `sub`, `aud`, `iat`, `exp` and `jti` take precedence over overlapping entries. |
155
154
 
156
155
  Keys must belong to an externally registered `clientId`, or a custom `clientMetadataUrl` whose document describes `private_key_jwt` and provides the matching public verification key. The SDK signs issuer and subject as the actual selected client ID. Configured and usable stored clients retain their existing priority. If a previous login uses the shared method-`none` browser identity or a shared secret, private-key authentication fails before key resolution instead of silently migrating or erasing that login. `clientSecret` and `privateKeyJwt` cannot be configured together. The shared Fitch document cannot identify a private-key client. Explicit pre-registration and opaque DCR client IDs are not classified as shared CIMD merely because their strings resemble a document URL.
157
156
 
@@ -197,6 +196,7 @@ Run the `/mcp-auth` command with the server name:
197
196
  Manual `/mcp-auth` is the default flow. With `settings.autoAuth: true`, proxy/direct/script tool execution may authenticate when a server needs auth or rejects a call for insufficient scope. Browser authorization still requires an interactive host, and each invocation gets at most one automatic auth attempt and one post-auth retry. Initial sign-in does not grant a second automatic consent attempt in the same invocation.
198
197
 
199
198
  For browser authorization, this will:
199
+
200
200
  1. Start the callback server lazily on an OS-assigned local port, or on the exact `oauth.redirectUri` port for pre-registered callbacks
201
201
  2. Discover OAuth endpoints automatically
202
202
  3. Select a configured/stored client, CIMD, or dynamic registration
@@ -253,6 +253,7 @@ mcp({ action: "call", server: "my-oauth-server", tool: "my-tool", args: { key: "
253
253
  ```
254
254
 
255
255
  The SDK automatically:
256
+
256
257
  - Adds the access token to requests
257
258
  - Refreshes expired tokens automatically
258
259
  - Re-authenticates if tokens are invalid
@@ -434,12 +435,8 @@ The default remains strict, including when `protocolVersion` is `"legacy"`. The
434
435
  The implementation uses the official split MCP SDK v2 client and OAuth APIs. MCP Apps also uses this split SDK graph; SDK v1 is not a runtime dependency:
435
436
 
436
437
  ```typescript
437
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
438
- import {
439
- auth,
440
- UnauthorizedError,
441
- type OAuthClientProvider,
442
- } from "@modelcontextprotocol/client"
438
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
439
+ import { auth, UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/client";
443
440
  ```
444
441
 
445
442
  The `McpOAuthProvider` class implements `OAuthClientProvider` and is passed to `StreamableHTTPClientTransport`:
@@ -447,7 +444,7 @@ The `McpOAuthProvider` class implements `OAuthClientProvider` and is passed to `
447
444
  ```typescript
448
445
  const transport = new StreamableHTTPClientTransport(url, {
449
446
  authProvider: new McpOAuthProvider(serverName, serverUrl, config, callbacks),
450
- })
447
+ });
451
448
  ```
452
449
 
453
450
  ## References
package/README.md CHANGED
@@ -44,14 +44,14 @@ By default, servers connect when Pi discovers an uncached catalog or calls a too
44
44
 
45
45
  ## Controls and settings
46
46
 
47
- | Command | Purpose |
48
- | --- | --- |
49
- | `/mcp` | Show servers and pin tools |
50
- | `/mcp status` | Show connection state and tool/resource counts |
51
- | `/mcp reconnect <server>` | Reconnect and refresh the catalog |
52
- | `/mcp-auth <server>` | Start OAuth in an interactive session |
53
- | `/mcp disable <server>` | Disable the server for this project; apply with `/reload` |
54
- | `/mcp enable <server>` | Enable the server for this project; apply with `/reload` |
47
+ | Command | Purpose |
48
+ | ------------------------- | --------------------------------------------------------- |
49
+ | `/mcp` | Show servers and pin tools |
50
+ | `/mcp status` | Show connection state and tool/resource counts |
51
+ | `/mcp reconnect <server>` | Reconnect and refresh the catalog |
52
+ | `/mcp-auth <server>` | Start OAuth in an interactive session |
53
+ | `/mcp disable <server>` | Disable the server for this project; apply with `/reload` |
54
+ | `/mcp enable <server>` | Enable the server for this project; apply with `/reload` |
55
55
 
56
56
  Use [`directTools`](docs/reference.md#typed-discovery-and-pinned-tools) to pin tools at startup. Other tools remain available for discovery. Global search covers known catalogs; select a server to discover an unknown catalog.
57
57
 
package/cli.js CHANGED
@@ -8,9 +8,14 @@ import stripJsonComments from "strip-json-comments";
8
8
 
9
9
  const HOME = os.homedir();
10
10
 
11
+ /** @param {string} input @returns {string} */
11
12
  function expandHome(input) {
12
- if (input === "~") return HOME;
13
- if (input.startsWith("~/")) return path.resolve(HOME, input.slice(2));
13
+ if (input === "~") {
14
+ return HOME;
15
+ }
16
+ if (input.startsWith("~/")) {
17
+ return path.resolve(HOME, input.slice(2));
18
+ }
14
19
  return path.resolve(input);
15
20
  }
16
21
 
@@ -31,11 +36,10 @@ const IMPORT_PATHS = {
31
36
  path.join(HOME, ".claude.json"),
32
37
  path.join(HOME, ".claude", "claude_desktop_config.json"),
33
38
  ],
34
- "claude-desktop": [path.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json")],
35
- codex: [
36
- path.join(HOME, ".codex", "config.toml"),
37
- path.join(HOME, ".codex", "config.json"),
39
+ "claude-desktop": [
40
+ path.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
38
41
  ],
42
+ codex: [path.join(HOME, ".codex", "config.toml"), path.join(HOME, ".codex", "config.json")],
39
43
  opencode: [
40
44
  path.join(HOME, ".config", "opencode", "opencode.json"),
41
45
  path.resolve(process.cwd(), "opencode.json"),
@@ -55,8 +59,11 @@ function printHelp(log = console.log) {
55
59
  log(" fitch-mcp-adapter migrate [--dry-run] Copy pre-v5 state without changing its sources");
56
60
  }
57
61
 
62
+ /** @param {string} filePath */
58
63
  function readJsonFile(filePath) {
59
- return JSON.parse(stripJsonComments(fs.readFileSync(filePath, "utf-8"), { trailingCommas: true }));
64
+ return JSON.parse(
65
+ stripJsonComments(fs.readFileSync(filePath, "utf-8"), { trailingCommas: true }),
66
+ );
60
67
  }
61
68
 
62
69
  function loadPiConfig() {
@@ -67,13 +74,17 @@ function loadPiConfig() {
67
74
  const raw = readJsonFile(PI_CONFIG_PATH);
68
75
  const mcpServers = raw.mcpServers ?? raw["mcp-servers"] ?? {};
69
76
  if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
70
- throw new Error(`Invalid MCP config at ${PI_CONFIG_PATH}: expected \"mcpServers\" to be an object`);
77
+ throw new Error(
78
+ `Invalid MCP config at ${PI_CONFIG_PATH}: expected "mcpServers" to be an object`,
79
+ );
71
80
  }
72
81
 
73
82
  const normalized = { ...raw };
74
83
  delete normalized["mcp-servers"];
75
84
 
76
- const imports = Array.isArray(raw.imports) ? raw.imports.filter((value) => typeof value === "string") : undefined;
85
+ const imports = Array.isArray(raw.imports)
86
+ ? raw.imports.filter((/** @type {unknown} */ value) => typeof value === "string")
87
+ : undefined;
77
88
  return {
78
89
  ...normalized,
79
90
  mcpServers,
@@ -94,6 +105,7 @@ function findAvailableImports() {
94
105
  return found;
95
106
  }
96
107
 
108
+ /** @param {(line: string) => void} log @param {{ kind: string; path: string }[]} imports */
97
109
  function printDiscovery(log, imports) {
98
110
  log("Config discovery:\n");
99
111
 
@@ -107,6 +119,9 @@ function printDiscovery(log, imports) {
107
119
  ];
108
120
 
109
121
  for (const [label, filePath] of paths) {
122
+ if (filePath === undefined) {
123
+ continue;
124
+ }
110
125
  const prefix = fs.existsSync(filePath) ? "✓" : "-";
111
126
  log(`${prefix} ${label}: ${filePath}`);
112
127
  }
@@ -122,11 +137,13 @@ function printDiscovery(log, imports) {
122
137
  }
123
138
  }
124
139
 
140
+ /** @param {{ imports?: unknown }} config */
125
141
  function writePiConfig(config) {
126
142
  fs.mkdirSync(path.dirname(PI_CONFIG_PATH), { recursive: true });
127
143
  fs.writeFileSync(PI_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
128
144
  }
129
145
 
146
+ /** @param {string[]} argv @param {(line: string) => void} [log] */
130
147
  async function runInit(argv, log = console.log) {
131
148
  const dryRun = argv.includes("--dry-run");
132
149
  const discoverHostConfigs = argv.includes("--discover-host-configs");
@@ -139,16 +156,21 @@ async function runInit(argv, log = console.log) {
139
156
 
140
157
  printDiscovery(log, foundImports);
141
158
 
142
- const discoverySettingChanged = discoverHostConfigs && existingConfig.settings?.hostConfigDiscovery !== "on";
159
+ const discoverySettingChanged =
160
+ discoverHostConfigs && existingConfig.settings?.hostConfigDiscovery !== "on";
143
161
  if (importsToAdd.length === 0 && !discoverySettingChanged) {
144
162
  log("\nNo Pi config changes needed.");
145
- log("Standard MCP configs are discovered automatically, and host-specific imports are already configured or unavailable.");
163
+ log(
164
+ "Standard MCP configs are discovered automatically, and host-specific imports are already configured or unavailable.",
165
+ );
146
166
  return 0;
147
167
  }
148
168
 
149
169
  const nextConfig = {
150
170
  ...existingConfig,
151
- ...(discoverySettingChanged ? { settings: { ...existingConfig.settings, hostConfigDiscovery: "on" } } : {}),
171
+ ...(discoverySettingChanged
172
+ ? { settings: { ...existingConfig.settings, hostConfigDiscovery: "on" } }
173
+ : {}),
152
174
  ...(importsToAdd.length > 0 ? { imports: [...existingImports, ...importsToAdd] } : {}),
153
175
  mcpServers: existingConfig.mcpServers ?? {},
154
176
  };
@@ -157,7 +179,9 @@ async function runInit(argv, log = console.log) {
157
179
  log(`\nDetected host configs to import into Pi: ${importsToAdd.join(", ")}`);
158
180
  }
159
181
  if (discoverySettingChanged) {
160
- log("Opting in to host-specific fallback discovery (standard and Pi-owned configs still take precedence).");
182
+ log(
183
+ "Opting in to host-specific fallback discovery (standard and Pi-owned configs still take precedence).",
184
+ );
161
185
  }
162
186
 
163
187
  if (dryRun) {
@@ -167,9 +191,13 @@ async function runInit(argv, log = console.log) {
167
191
 
168
192
  writePiConfig(nextConfig);
169
193
  log(`Updated ${PI_CONFIG_PATH}`);
170
- log("Pi will now keep reading standard MCP configs automatically, while these imports cover host-specific config formats.");
194
+ log(
195
+ "Pi will now keep reading standard MCP configs automatically, while these imports cover host-specific config formats.",
196
+ );
171
197
  if (discoverySettingChanged) {
172
- log("Host config discovery is explicit and does not write to or execute commands from external host files.");
198
+ log(
199
+ "Host config discovery is explicit and does not write to or execute commands from external host files.",
200
+ );
173
201
  }
174
202
  return 0;
175
203
  }
@@ -184,7 +212,9 @@ export async function main(argv = process.argv.slice(2), log = console.log, erro
184
212
 
185
213
  if (command === "install") {
186
214
  error("The custom downloader has been retired.");
187
- error("Use `pi install npm:@fitchmultz/pi-mcp-adapter` instead, then optionally run `fitch-mcp-adapter init`.");
215
+ error(
216
+ "Use `pi install npm:@fitchmultz/pi-mcp-adapter` instead, then optionally run `fitch-mcp-adapter init`.",
217
+ );
188
218
  return 1;
189
219
  }
190
220
 
@@ -196,11 +226,17 @@ export async function main(argv = process.argv.slice(2), log = console.log, erro
196
226
  const { migrateLegacyState } = await import("./dist/legacy-migration.js");
197
227
  const dryRun = rest.includes("--dry-run");
198
228
  const result = migrateLegacyState({ dryRun });
199
- for (const file of result.files) log(`${file.status}: ${file.source} -> ${file.destination}`);
200
- for (const credential of result.credentials) log(`OAuth ${credential.server}: ${credential.status}`);
201
- log(dryRun
202
- ? "Dry run: no files changed and no credential store accessed."
203
- : "Migration complete. Existing destinations and all source files/credentials were left intact. Restart Pi to use the independent adapter.");
229
+ for (const file of result.files) {
230
+ log(`${file.status}: ${file.source} -> ${file.destination}`);
231
+ }
232
+ for (const credential of result.credentials) {
233
+ log(`OAuth ${credential.server}: ${credential.status}`);
234
+ }
235
+ log(
236
+ dryRun
237
+ ? "Dry run: no files changed and no credential store accessed."
238
+ : "Migration complete. Existing destinations and all source files/credentials were left intact. Restart Pi to use the independent adapter.",
239
+ );
204
240
  return 0;
205
241
  }
206
242
 
@@ -210,13 +246,16 @@ export async function main(argv = process.argv.slice(2), log = console.log, erro
210
246
  }
211
247
 
212
248
  const resolvedEntrypoint = process.argv[1] ? fs.realpathSync(process.argv[1]) : undefined;
213
- const isEntrypoint = resolvedEntrypoint && import.meta.url === pathToFileURL(resolvedEntrypoint).href;
249
+ const isEntrypoint =
250
+ resolvedEntrypoint && import.meta.url === pathToFileURL(resolvedEntrypoint).href;
214
251
 
215
252
  if (isEntrypoint) {
216
- main().then((code) => {
217
- process.exitCode = code;
218
- }).catch((err) => {
219
- console.error(`\nHelper failed: ${err instanceof Error ? err.message : String(err)}`);
220
- process.exit(1);
221
- });
253
+ main()
254
+ .then((code) => {
255
+ process.exitCode = code;
256
+ })
257
+ .catch((err) => {
258
+ console.error(`\nHelper failed: ${err instanceof Error ? err.message : String(err)}`);
259
+ process.exit(1);
260
+ });
222
261
  }
package/dist/abort.js CHANGED
@@ -1,32 +1,41 @@
1
1
  export function throwIfAborted(signal) {
2
- if (!signal?.aborted)
2
+ if (!signal?.aborted) {
3
3
  return;
4
- throw signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? "MCP request aborted"));
4
+ }
5
+ throw signal.reason instanceof Error
6
+ ? signal.reason
7
+ : new Error(String(signal.reason ?? "MCP request aborted"));
5
8
  }
6
9
  export async function abortable(promise, signal) {
7
- if (!signal)
10
+ if (!signal) {
8
11
  return promise;
12
+ }
9
13
  throwIfAborted(signal);
10
14
  return await new Promise((resolve, reject) => {
11
15
  let settled = false;
12
16
  const cleanup = () => signal.removeEventListener("abort", onAbort);
13
17
  const onAbort = () => {
14
- if (settled)
18
+ if (settled) {
15
19
  return;
20
+ }
16
21
  settled = true;
17
22
  cleanup();
18
- reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? "MCP request aborted")));
23
+ reject(signal.reason instanceof Error
24
+ ? signal.reason
25
+ : new Error(String(signal.reason ?? "MCP request aborted")));
19
26
  };
20
27
  signal.addEventListener("abort", onAbort, { once: true });
21
- promise.then(value => {
22
- if (settled)
28
+ promise.then((value) => {
29
+ if (settled) {
23
30
  return;
31
+ }
24
32
  settled = true;
25
33
  cleanup();
26
34
  resolve(value);
27
- }, error => {
28
- if (settled)
35
+ }, (error) => {
36
+ if (settled) {
29
37
  return;
38
+ }
30
39
  settled = true;
31
40
  cleanup();
32
41
  reject(error);
@@ -4,16 +4,18 @@ export async function drainClientCallbacks(client) {
4
4
  const pending = [...(clientOperations.get(client) ?? [])]
5
5
  .filter(([, kind]) => kind === "sampling callback" || kind === "elicitation callback")
6
6
  .map(([operation]) => operation);
7
- if (!pending.length)
7
+ if (!pending.length) {
8
8
  return;
9
+ }
9
10
  await Promise.allSettled(pending);
10
11
  }
11
12
  }
12
13
  export async function drainClientOperations(client) {
13
14
  for (;;) {
14
15
  const pending = [...(clientOperations.get(client)?.keys() ?? [])];
15
- if (!pending.length)
16
+ if (!pending.length) {
16
17
  return;
18
+ }
17
19
  await Promise.allSettled(pending);
18
20
  }
19
21
  }