@openclaw/google-meet 2026.5.1-beta.2 → 2026.5.2-beta.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/index.create.test.ts +43 -2
- package/index.test.ts +220 -1
- package/index.ts +129 -4
- package/openclaw.plugin.json +3 -0
- package/package.json +4 -4
- package/src/cli.test.ts +107 -0
- package/src/cli.ts +83 -2
- package/src/create.ts +56 -6
- package/src/meet.ts +69 -7
- package/src/oauth.ts +6 -5
- package/src/runtime.ts +133 -3
- package/src/setup.ts +17 -0
- package/src/test-support/plugin-harness.ts +11 -1
- package/src/transports/types.ts +1 -0
- package/src/voice-call-gateway.test.ts +24 -4
- package/src/voice-call-gateway.ts +64 -6
- package/dist/.boundary-tsc.stamp +0 -1
- package/dist/.boundary-tsc.tsbuildinfo +0 -1
package/src/runtime.ts
CHANGED
|
@@ -66,6 +66,43 @@ function hasRealtimeAudioOutputAdvanced(
|
|
|
66
66
|
return (health?.lastOutputBytes ?? 0) > startOutputBytes;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
type TranscriptCheckpoint = {
|
|
70
|
+
lines: number;
|
|
71
|
+
lastCaptionAt?: string;
|
|
72
|
+
lastCaptionText?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function transcriptCheckpoint(health: GoogleMeetChromeHealth | undefined): TranscriptCheckpoint {
|
|
76
|
+
return {
|
|
77
|
+
lines: health?.transcriptLines ?? 0,
|
|
78
|
+
lastCaptionAt: health?.lastCaptionAt,
|
|
79
|
+
lastCaptionText: health?.lastCaptionText,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasTranscriptAdvanced(
|
|
84
|
+
health: GoogleMeetChromeHealth | undefined,
|
|
85
|
+
start: TranscriptCheckpoint,
|
|
86
|
+
): boolean {
|
|
87
|
+
if ((health?.transcriptLines ?? 0) > start.lines) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (health?.lastCaptionAt && health.lastCaptionAt !== start.lastCaptionAt) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return Boolean(health?.lastCaptionText && health.lastCaptionText !== start.lastCaptionText);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveProbeTimeoutMs(input: number | undefined, fallback: number): number {
|
|
97
|
+
if (input === undefined) {
|
|
98
|
+
return Math.min(Math.max(fallback, 1), 120_000);
|
|
99
|
+
}
|
|
100
|
+
if (!Number.isFinite(input) || input <= 0) {
|
|
101
|
+
throw new Error("timeoutMs must be a positive number");
|
|
102
|
+
}
|
|
103
|
+
return Math.min(Math.trunc(input), 120_000);
|
|
104
|
+
}
|
|
105
|
+
|
|
69
106
|
function sleep(ms: number): Promise<void> {
|
|
70
107
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
108
|
}
|
|
@@ -189,9 +226,17 @@ export class GoogleMeetRuntime {
|
|
|
189
226
|
return session ? { found: true, session } : { found: false };
|
|
190
227
|
}
|
|
191
228
|
|
|
192
|
-
async setupStatus(
|
|
229
|
+
async setupStatus(
|
|
230
|
+
options: {
|
|
231
|
+
transport?: GoogleMeetTransport;
|
|
232
|
+
mode?: GoogleMeetMode;
|
|
233
|
+
dialInNumber?: string;
|
|
234
|
+
} = {},
|
|
235
|
+
) {
|
|
193
236
|
const transport = resolveTransport(options.transport, this.params.config);
|
|
194
237
|
const mode = resolveMode(options.mode, this.params.config);
|
|
238
|
+
const twilioDialInNumber =
|
|
239
|
+
transport === "twilio" ? normalizeDialInNumber(options.dialInNumber) : undefined;
|
|
195
240
|
const shouldCheckChromeNode =
|
|
196
241
|
transport === "chrome-node" ||
|
|
197
242
|
(!options.transport && Boolean(this.params.config.chromeNode.node));
|
|
@@ -199,6 +244,7 @@ export class GoogleMeetRuntime {
|
|
|
199
244
|
fullConfig: this.params.fullConfig,
|
|
200
245
|
mode,
|
|
201
246
|
transport,
|
|
247
|
+
twilioDialInNumber,
|
|
202
248
|
});
|
|
203
249
|
if (shouldCheckChromeNode) {
|
|
204
250
|
try {
|
|
@@ -403,7 +449,9 @@ export class GoogleMeetRuntime {
|
|
|
403
449
|
request.dialInNumber ?? this.params.config.twilio.defaultDialInNumber,
|
|
404
450
|
);
|
|
405
451
|
if (!dialInNumber) {
|
|
406
|
-
throw new Error(
|
|
452
|
+
throw new Error(
|
|
453
|
+
"Twilio transport requires a Meet dial-in phone number. Google Meet URLs do not include dial-in details; pass dialInNumber with optional pin/dtmfSequence, configure twilio.defaultDialInNumber, or use chrome/chrome-node transport.",
|
|
454
|
+
);
|
|
407
455
|
}
|
|
408
456
|
const dtmfSequence = buildMeetDtmfSequence({
|
|
409
457
|
pin: request.pin ?? this.params.config.twilio.defaultPin,
|
|
@@ -443,7 +491,7 @@ export class GoogleMeetRuntime {
|
|
|
443
491
|
session.notes.push(
|
|
444
492
|
this.params.config.voiceCall.enabled
|
|
445
493
|
? dtmfSequence
|
|
446
|
-
? "Twilio transport delegated the
|
|
494
|
+
? "Twilio transport delegated the phone leg to the voice-call plugin, then sent configured DTMF after connect before speaking."
|
|
447
495
|
: "Twilio transport delegated the call to the voice-call plugin without configured DTMF."
|
|
448
496
|
: "Twilio transport is an explicit dial plan; voice-call delegation is disabled.",
|
|
449
497
|
);
|
|
@@ -597,6 +645,88 @@ export class GoogleMeetRuntime {
|
|
|
597
645
|
};
|
|
598
646
|
}
|
|
599
647
|
|
|
648
|
+
async testListen(request: GoogleMeetJoinRequest): Promise<{
|
|
649
|
+
createdSession: boolean;
|
|
650
|
+
inCall?: boolean;
|
|
651
|
+
manualActionRequired?: boolean;
|
|
652
|
+
manualActionReason?: GoogleMeetChromeHealth["manualActionReason"];
|
|
653
|
+
manualActionMessage?: string;
|
|
654
|
+
listenVerified: boolean;
|
|
655
|
+
listenTimedOut: boolean;
|
|
656
|
+
captioning?: boolean;
|
|
657
|
+
captionsEnabledAttempted?: boolean;
|
|
658
|
+
transcriptLines?: number;
|
|
659
|
+
lastCaptionAt?: string;
|
|
660
|
+
lastCaptionSpeaker?: string;
|
|
661
|
+
lastCaptionText?: string;
|
|
662
|
+
recentTranscript?: GoogleMeetChromeHealth["recentTranscript"];
|
|
663
|
+
session: GoogleMeetSession;
|
|
664
|
+
}> {
|
|
665
|
+
if (request.mode === "realtime") {
|
|
666
|
+
throw new Error(
|
|
667
|
+
"test_listen requires mode: transcribe; use test_speech for realtime talk-back.",
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
const url = normalizeMeetUrl(request.url);
|
|
671
|
+
const transport = resolveTransport(request.transport, this.params.config);
|
|
672
|
+
if (transport === "twilio") {
|
|
673
|
+
throw new Error("test_listen supports chrome or chrome-node transports");
|
|
674
|
+
}
|
|
675
|
+
const beforeSessions = this.list();
|
|
676
|
+
const before = new Set(beforeSessions.map((session) => session.id));
|
|
677
|
+
const existingSession = beforeSessions.find(
|
|
678
|
+
(session) =>
|
|
679
|
+
session.state === "active" &&
|
|
680
|
+
isSameMeetUrlForReuse(session.url, url) &&
|
|
681
|
+
session.transport === transport &&
|
|
682
|
+
session.mode === "transcribe",
|
|
683
|
+
);
|
|
684
|
+
const start = transcriptCheckpoint(existingSession?.chrome?.health);
|
|
685
|
+
const result = await this.join({
|
|
686
|
+
...request,
|
|
687
|
+
transport,
|
|
688
|
+
url,
|
|
689
|
+
mode: "transcribe",
|
|
690
|
+
message: undefined,
|
|
691
|
+
});
|
|
692
|
+
let health = result.session.chrome?.health;
|
|
693
|
+
const timeoutMs = resolveProbeTimeoutMs(
|
|
694
|
+
request.timeoutMs,
|
|
695
|
+
this.params.config.chrome.joinTimeoutMs,
|
|
696
|
+
);
|
|
697
|
+
const shouldWait =
|
|
698
|
+
health?.manualActionRequired !== true && isManagedChromeBrowserSession(result.session);
|
|
699
|
+
if (shouldWait && !hasTranscriptAdvanced(health, start)) {
|
|
700
|
+
const deadline = Date.now() + timeoutMs;
|
|
701
|
+
while (Date.now() < deadline) {
|
|
702
|
+
await sleep(250);
|
|
703
|
+
await this.#refreshCaptionHealthForSession(result.session);
|
|
704
|
+
health = result.session.chrome?.health;
|
|
705
|
+
if (health?.manualActionRequired || hasTranscriptAdvanced(health, start)) {
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const listenVerified = hasTranscriptAdvanced(health, start);
|
|
711
|
+
return {
|
|
712
|
+
createdSession: !before.has(result.session.id),
|
|
713
|
+
inCall: health?.inCall,
|
|
714
|
+
manualActionRequired: health?.manualActionRequired,
|
|
715
|
+
manualActionReason: health?.manualActionReason,
|
|
716
|
+
manualActionMessage: health?.manualActionMessage,
|
|
717
|
+
listenVerified,
|
|
718
|
+
listenTimedOut: shouldWait && !listenVerified && health?.manualActionRequired !== true,
|
|
719
|
+
captioning: health?.captioning,
|
|
720
|
+
captionsEnabledAttempted: health?.captionsEnabledAttempted,
|
|
721
|
+
transcriptLines: health?.transcriptLines,
|
|
722
|
+
lastCaptionAt: health?.lastCaptionAt,
|
|
723
|
+
lastCaptionSpeaker: health?.lastCaptionSpeaker,
|
|
724
|
+
lastCaptionText: health?.lastCaptionText,
|
|
725
|
+
recentTranscript: health?.recentTranscript,
|
|
726
|
+
session: result.session,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
600
730
|
async #refreshCaptionHealthForSession(session: GoogleMeetSession) {
|
|
601
731
|
if (session.mode !== "transcribe") {
|
|
602
732
|
this.#refreshSpeechReadiness(session);
|
package/src/setup.ts
CHANGED
|
@@ -87,6 +87,7 @@ export function getGoogleMeetSetupStatus(
|
|
|
87
87
|
fullConfig?: unknown;
|
|
88
88
|
mode?: GoogleMeetMode;
|
|
89
89
|
transport?: GoogleMeetTransport;
|
|
90
|
+
twilioDialInNumber?: string;
|
|
90
91
|
},
|
|
91
92
|
): {
|
|
92
93
|
ok: boolean;
|
|
@@ -99,6 +100,7 @@ export function getGoogleMeetSetupStatus(
|
|
|
99
100
|
fullConfig?: unknown;
|
|
100
101
|
mode?: GoogleMeetMode;
|
|
101
102
|
transport?: GoogleMeetTransport;
|
|
103
|
+
twilioDialInNumber?: string;
|
|
102
104
|
},
|
|
103
105
|
) {
|
|
104
106
|
const checks: SetupCheck[] = [];
|
|
@@ -193,6 +195,21 @@ export function getGoogleMeetSetupStatus(
|
|
|
193
195
|
});
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
if (transport === "twilio") {
|
|
199
|
+
const hasRequestDialPlan = Boolean(options?.twilioDialInNumber);
|
|
200
|
+
const hasDefaultDialPlan = Boolean(config.twilio.defaultDialInNumber);
|
|
201
|
+
const hasDialPlan = hasRequestDialPlan || hasDefaultDialPlan;
|
|
202
|
+
checks.push({
|
|
203
|
+
id: "twilio-dial-plan",
|
|
204
|
+
ok: hasDialPlan,
|
|
205
|
+
message: hasRequestDialPlan
|
|
206
|
+
? "Twilio request includes a Meet dial-in number"
|
|
207
|
+
: hasDefaultDialPlan
|
|
208
|
+
? "Twilio default Meet dial-in number is configured"
|
|
209
|
+
: "Twilio joins require a Meet dial-in phone number; pass dialInNumber with optional pin/dtmfSequence or configure twilio.defaultDialInNumber",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
const shouldCheckTwilioDelegation =
|
|
197
214
|
config.voiceCall.enabled &&
|
|
198
215
|
(transport === "twilio" ||
|
|
@@ -60,6 +60,7 @@ export function setupGoogleMeetPlugin(
|
|
|
60
60
|
argv: string[],
|
|
61
61
|
options?: { timeoutMs?: number },
|
|
62
62
|
) => Promise<CommandResult>;
|
|
63
|
+
registerPlatform?: NodeJS.Platform;
|
|
63
64
|
} = {},
|
|
64
65
|
) {
|
|
65
66
|
const methods = new Map<string, unknown>();
|
|
@@ -157,7 +158,16 @@ export function setupGoogleMeetPlugin(
|
|
|
157
158
|
registerCli: (_registrar: unknown, opts: unknown) => cliRegistrations.push(opts),
|
|
158
159
|
registerNodeHostCommand: (command: unknown) => nodeHostCommands.push(command),
|
|
159
160
|
});
|
|
160
|
-
|
|
161
|
+
const originalPlatform = process.platform;
|
|
162
|
+
Object.defineProperty(process, "platform", {
|
|
163
|
+
configurable: true,
|
|
164
|
+
value: options.registerPlatform ?? "darwin",
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
plugin.register(api);
|
|
168
|
+
} finally {
|
|
169
|
+
Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
|
|
170
|
+
}
|
|
161
171
|
return {
|
|
162
172
|
cliRegistrations,
|
|
163
173
|
methods,
|
package/src/transports/types.ts
CHANGED
|
@@ -21,39 +21,59 @@ vi.mock("openclaw/plugin-sdk/gateway-runtime", () => ({
|
|
|
21
21
|
|
|
22
22
|
describe("Google Meet voice-call gateway", () => {
|
|
23
23
|
beforeEach(() => {
|
|
24
|
+
vi.useRealTimers();
|
|
24
25
|
gatewayMocks.request.mockReset();
|
|
25
26
|
gatewayMocks.request.mockResolvedValue({ callId: "call-1" });
|
|
26
27
|
gatewayMocks.stopAndWait.mockClear();
|
|
27
28
|
gatewayMocks.startGatewayClientWhenEventLoopReady.mockClear();
|
|
28
29
|
});
|
|
29
30
|
|
|
30
|
-
it("starts Twilio Meet calls
|
|
31
|
+
it("starts Twilio Meet calls, sends delayed DTMF, then speaks the intro", async () => {
|
|
31
32
|
const config = resolveGoogleMeetConfig({
|
|
32
33
|
voiceCall: {
|
|
33
34
|
gatewayUrl: "ws://127.0.0.1:18789",
|
|
34
35
|
dtmfDelayMs: 1,
|
|
36
|
+
postDtmfSpeechDelayMs: 2,
|
|
35
37
|
},
|
|
36
38
|
realtime: { introMessage: "Say exactly: I'm here and listening." },
|
|
37
39
|
});
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
const join = joinMeetViaVoiceCallGateway({
|
|
40
42
|
config,
|
|
41
43
|
dialInNumber: "+15551234567",
|
|
42
44
|
dtmfSequence: "123456#",
|
|
43
45
|
message: "Say exactly: I'm here and listening.",
|
|
44
46
|
});
|
|
45
47
|
|
|
48
|
+
await join;
|
|
49
|
+
|
|
46
50
|
expect(gatewayMocks.request).toHaveBeenNthCalledWith(
|
|
47
51
|
1,
|
|
48
52
|
"voicecall.start",
|
|
49
53
|
{
|
|
50
54
|
to: "+15551234567",
|
|
51
55
|
mode: "conversation",
|
|
56
|
+
},
|
|
57
|
+
{ timeoutMs: 30_000 },
|
|
58
|
+
);
|
|
59
|
+
expect(gatewayMocks.request).toHaveBeenNthCalledWith(
|
|
60
|
+
2,
|
|
61
|
+
"voicecall.dtmf",
|
|
62
|
+
{
|
|
63
|
+
callId: "call-1",
|
|
64
|
+
digits: "123456#",
|
|
65
|
+
},
|
|
66
|
+
{ timeoutMs: 30_000 },
|
|
67
|
+
);
|
|
68
|
+
expect(gatewayMocks.request).toHaveBeenNthCalledWith(
|
|
69
|
+
3,
|
|
70
|
+
"voicecall.speak",
|
|
71
|
+
{
|
|
72
|
+
callId: "call-1",
|
|
52
73
|
message: "Say exactly: I'm here and listening.",
|
|
53
|
-
dtmfSequence: "123456#",
|
|
54
74
|
},
|
|
55
75
|
{ timeoutMs: 30_000 },
|
|
56
76
|
);
|
|
57
|
-
expect(gatewayMocks.request).toHaveBeenCalledTimes(
|
|
77
|
+
expect(gatewayMocks.request).toHaveBeenCalledTimes(3);
|
|
58
78
|
});
|
|
59
79
|
});
|
|
@@ -18,12 +18,24 @@ type VoiceCallSpeakResult = {
|
|
|
18
18
|
error?: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
type VoiceCallDtmfResult = {
|
|
22
|
+
success?: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
21
26
|
type VoiceCallMeetJoinResult = {
|
|
22
27
|
callId: string;
|
|
23
28
|
dtmfSent: boolean;
|
|
24
29
|
introSent: boolean;
|
|
25
30
|
};
|
|
26
31
|
|
|
32
|
+
function sleep(ms: number): Promise<void> {
|
|
33
|
+
if (ms <= 0) {
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
async function createConnectedGatewayClient(
|
|
28
40
|
config: GoogleMeetConfig,
|
|
29
41
|
): Promise<VoiceCallGatewayClient> {
|
|
@@ -81,15 +93,13 @@ export async function joinMeetViaVoiceCallGateway(params: {
|
|
|
81
93
|
try {
|
|
82
94
|
client = await createConnectedGatewayClient(params.config);
|
|
83
95
|
params.logger?.info(
|
|
84
|
-
`[google-meet] Delegating Twilio join to Voice Call (dtmf=${params.dtmfSequence ? "
|
|
96
|
+
`[google-meet] Delegating Twilio join to Voice Call (dtmf=${params.dtmfSequence ? "post-connect" : "none"}, intro=${params.message ? "delayed" : "none"})`,
|
|
85
97
|
);
|
|
86
98
|
const start = (await client.request(
|
|
87
99
|
"voicecall.start",
|
|
88
100
|
{
|
|
89
101
|
to: params.dialInNumber,
|
|
90
102
|
mode: "conversation",
|
|
91
|
-
...(params.message ? { message: params.message } : {}),
|
|
92
|
-
...(params.dtmfSequence ? { dtmfSequence: params.dtmfSequence } : {}),
|
|
93
103
|
},
|
|
94
104
|
{ timeoutMs: params.config.voiceCall.requestTimeoutMs },
|
|
95
105
|
)) as VoiceCallStartResult;
|
|
@@ -97,12 +107,60 @@ export async function joinMeetViaVoiceCallGateway(params: {
|
|
|
97
107
|
throw new Error(start.error || "voicecall.start did not return callId");
|
|
98
108
|
}
|
|
99
109
|
params.logger?.info(
|
|
100
|
-
`[google-meet] Voice Call Twilio
|
|
110
|
+
`[google-meet] Voice Call Twilio phone leg started: callId=${start.callId}`,
|
|
101
111
|
);
|
|
112
|
+
let dtmfSent = false;
|
|
113
|
+
if (params.dtmfSequence) {
|
|
114
|
+
const delayMs = params.config.voiceCall.dtmfDelayMs;
|
|
115
|
+
params.logger?.info(
|
|
116
|
+
`[google-meet] Waiting ${delayMs}ms before sending Meet DTMF for callId=${start.callId}`,
|
|
117
|
+
);
|
|
118
|
+
await sleep(delayMs);
|
|
119
|
+
const dtmf = (await client.request(
|
|
120
|
+
"voicecall.dtmf",
|
|
121
|
+
{
|
|
122
|
+
callId: start.callId,
|
|
123
|
+
digits: params.dtmfSequence,
|
|
124
|
+
},
|
|
125
|
+
{ timeoutMs: params.config.voiceCall.requestTimeoutMs },
|
|
126
|
+
)) as VoiceCallDtmfResult;
|
|
127
|
+
if (dtmf.success === false) {
|
|
128
|
+
throw new Error(dtmf.error || "voicecall.dtmf failed");
|
|
129
|
+
}
|
|
130
|
+
dtmfSent = true;
|
|
131
|
+
params.logger?.info(
|
|
132
|
+
`[google-meet] Meet DTMF sent after phone leg connected: callId=${start.callId} digits=${params.dtmfSequence.length}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
let introSent = false;
|
|
136
|
+
if (params.message) {
|
|
137
|
+
const delayMs = params.dtmfSequence ? params.config.voiceCall.postDtmfSpeechDelayMs : 0;
|
|
138
|
+
if (delayMs > 0) {
|
|
139
|
+
params.logger?.info(
|
|
140
|
+
`[google-meet] Waiting ${delayMs}ms after Meet DTMF before speaking intro for callId=${start.callId}`,
|
|
141
|
+
);
|
|
142
|
+
await sleep(delayMs);
|
|
143
|
+
}
|
|
144
|
+
const spoken = (await client.request(
|
|
145
|
+
"voicecall.speak",
|
|
146
|
+
{
|
|
147
|
+
callId: start.callId,
|
|
148
|
+
message: params.message,
|
|
149
|
+
},
|
|
150
|
+
{ timeoutMs: params.config.voiceCall.requestTimeoutMs },
|
|
151
|
+
)) as VoiceCallSpeakResult;
|
|
152
|
+
if (spoken.success === false) {
|
|
153
|
+
throw new Error(spoken.error || "voicecall.speak failed");
|
|
154
|
+
}
|
|
155
|
+
introSent = true;
|
|
156
|
+
params.logger?.info(
|
|
157
|
+
`[google-meet] Intro speech requested after Meet dial sequence: callId=${start.callId}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
102
160
|
return {
|
|
103
161
|
callId: start.callId,
|
|
104
|
-
dtmfSent
|
|
105
|
-
introSent
|
|
162
|
+
dtmfSent,
|
|
163
|
+
introSent,
|
|
106
164
|
};
|
|
107
165
|
} finally {
|
|
108
166
|
await client?.stopAndWait({ timeoutMs: 1_000 });
|
package/dist/.boundary-tsc.stamp
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2026-04-29T11:28:56.702Z
|