@minionry/sdk 0.4.23

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.
@@ -0,0 +1,11 @@
1
+ import type { Transport } from '../transport.js';
2
+ import type { MinionryProvider } from '../types.js';
3
+ export interface PostMessageTransportOptions {
4
+ hostOrigins?: string[];
5
+ timeoutMs?: number;
6
+ announceIntervalMs?: number;
7
+ requestTimeoutMs?: number;
8
+ signal?: AbortSignal;
9
+ }
10
+ export declare function createPostMessageTransport(options?: PostMessageTransportOptions): Promise<Transport>;
11
+ export declare function connectPostMessage(options?: PostMessageTransportOptions): Promise<MinionryProvider>;
@@ -0,0 +1,294 @@
1
+ import { HOST_CONNECT_TYPE, SDK_CONNECT_ACK_TYPE, SDK_EVENT_TYPE, SDK_HELLO_TYPE, SDK_NOTIFICATION_TYPE, SDK_PROTOCOL_VERSION, SDK_KEYDOWN_TYPE, SDK_REQUEST_TYPE, SDK_RESPONSE_TYPE, SDK_TOOL_CALL_TYPE, } from '../protocol.js';
2
+ import { createProvider } from '../provider.js';
3
+ import { MinionryError } from '../types.js';
4
+ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
5
+ const DEFAULT_ANNOUNCE_INTERVAL_MS = 250;
6
+ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
7
+ const INDEFINITE_METHODS = new Set([
8
+ 'connect',
9
+ 'agents.result',
10
+ 'notifications.request',
11
+ 'quality.result',
12
+ 'quality.installTools',
13
+ 'inference.result',
14
+ ]);
15
+ const isRecord = (data) => typeof data === 'object' && data !== null;
16
+ const isHostConnectFrame = (data) => {
17
+ if (!isRecord(data))
18
+ return false;
19
+ return (data.type === HOST_CONNECT_TYPE &&
20
+ data.sdkVersion === SDK_PROTOCOL_VERSION &&
21
+ typeof data.nonce === 'string' &&
22
+ data.nonce.length > 0);
23
+ };
24
+ const isResponseError = (data) => isRecord(data) && typeof data.code === 'string' && typeof data.message === 'string';
25
+ const warnDrop = (detail) => {
26
+ console.warn(`[@minionry/sdk] postMessage transport dropped a frame: ${detail}`);
27
+ };
28
+ const createPortTransport = (port, requestTimeoutMs) => {
29
+ let handlers;
30
+ let closed = false;
31
+ const requestTimers = new Map();
32
+ const clearRequestTimer = (requestId) => {
33
+ const timer = requestTimers.get(requestId);
34
+ if (timer !== undefined) {
35
+ clearTimeout(timer);
36
+ requestTimers.delete(requestId);
37
+ }
38
+ };
39
+ const shutdown = () => {
40
+ if (closed)
41
+ return;
42
+ closed = true;
43
+ for (const timer of requestTimers.values())
44
+ clearTimeout(timer);
45
+ requestTimers.clear();
46
+ port.removeEventListener('close', shutdown);
47
+ port.onmessage = null;
48
+ port.close();
49
+ handlers?.onClose();
50
+ };
51
+ port.addEventListener('close', shutdown);
52
+ const onPortMessage = (event) => {
53
+ if (closed || !handlers)
54
+ return;
55
+ const data = event.data;
56
+ if (!isRecord(data))
57
+ return;
58
+ switch (data.type) {
59
+ case SDK_RESPONSE_TYPE: {
60
+ if (typeof data.requestId !== 'string') {
61
+ warnDrop('response frame without a string requestId');
62
+ return;
63
+ }
64
+ if (data.ok === true) {
65
+ clearRequestTimer(data.requestId);
66
+ handlers.onResponse({ requestId: data.requestId, ok: true, result: data.result });
67
+ }
68
+ else if (data.ok === false && isResponseError(data.error)) {
69
+ clearRequestTimer(data.requestId);
70
+ handlers.onResponse({
71
+ requestId: data.requestId,
72
+ ok: false,
73
+ error: { code: data.error.code, message: data.error.message },
74
+ });
75
+ }
76
+ else {
77
+ warnDrop(`malformed response frame for requestId '${data.requestId}'`);
78
+ }
79
+ return;
80
+ }
81
+ case SDK_EVENT_TYPE: {
82
+ if (typeof data.requestId !== 'string' || typeof data.event !== 'string') {
83
+ warnDrop('malformed event frame');
84
+ return;
85
+ }
86
+ handlers.onEvent({
87
+ requestId: data.requestId,
88
+ event: data.event,
89
+ payload: data.payload,
90
+ ...(typeof data.seq === 'number' ? { seq: data.seq } : {}),
91
+ });
92
+ return;
93
+ }
94
+ case SDK_NOTIFICATION_TYPE: {
95
+ if (typeof data.event !== 'string') {
96
+ warnDrop('malformed notification frame');
97
+ return;
98
+ }
99
+ handlers.onNotification({ event: data.event, payload: data.payload });
100
+ return;
101
+ }
102
+ case SDK_TOOL_CALL_TYPE: {
103
+ if (typeof data.callId !== 'string' || typeof data.requestId !== 'string') {
104
+ warnDrop('tool-call frame without string callId/requestId');
105
+ return;
106
+ }
107
+ if (data.op === 'cancel') {
108
+ handlers.onToolCall?.({ op: 'cancel', callId: data.callId, requestId: data.requestId });
109
+ return;
110
+ }
111
+ if (data.op !== 'call' || typeof data.tool !== 'string') {
112
+ warnDrop(`malformed tool-call frame for callId '${data.callId}'`);
113
+ return;
114
+ }
115
+ handlers.onToolCall?.({
116
+ op: 'call',
117
+ callId: data.callId,
118
+ requestId: data.requestId,
119
+ tool: data.tool,
120
+ input: data.input,
121
+ });
122
+ return;
123
+ }
124
+ default:
125
+ return;
126
+ }
127
+ };
128
+ return {
129
+ start(startHandlers) {
130
+ if (closed)
131
+ throw new Error('postMessage transport is closed');
132
+ if (handlers)
133
+ throw new Error('postMessage transport already started — create one transport per provider');
134
+ handlers = startHandlers;
135
+ port.onmessage = onPortMessage;
136
+ },
137
+ send(frame) {
138
+ if (closed)
139
+ throw new Error('postMessage transport is closed');
140
+ if (!handlers)
141
+ throw new Error('postMessage transport not started');
142
+ const wire = { type: SDK_REQUEST_TYPE, requestId: frame.requestId, method: frame.method };
143
+ if (frame.params !== undefined)
144
+ wire.params = frame.params;
145
+ port.postMessage(wire);
146
+ if (requestTimeoutMs > 0 && !INDEFINITE_METHODS.has(frame.method)) {
147
+ const { requestId, method } = frame;
148
+ requestTimers.set(requestId, setTimeout(() => {
149
+ requestTimers.delete(requestId);
150
+ handlers?.onResponse({
151
+ requestId,
152
+ ok: false,
153
+ error: {
154
+ code: 'TIMEOUT',
155
+ message: `${method} timed out after ${requestTimeoutMs}ms — no response from the Minionry host`,
156
+ },
157
+ });
158
+ }, requestTimeoutMs));
159
+ }
160
+ },
161
+ sendKeydown(event) {
162
+ if (closed || !handlers)
163
+ return;
164
+ const wire = {
165
+ type: SDK_KEYDOWN_TYPE,
166
+ code: event.code,
167
+ key: event.key,
168
+ altKey: event.altKey,
169
+ ctrlKey: event.ctrlKey,
170
+ metaKey: event.metaKey,
171
+ shiftKey: event.shiftKey,
172
+ };
173
+ if (event.isComposing)
174
+ wire.isComposing = true;
175
+ port.postMessage(wire);
176
+ },
177
+ close() {
178
+ shutdown();
179
+ },
180
+ };
181
+ };
182
+ export function createPostMessageTransport(options = {}) {
183
+ const hostOrigins = options.hostOrigins?.length ? [...options.hostOrigins] : null;
184
+ const timeoutMs = options.timeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
185
+ const announceIntervalMs = options.announceIntervalMs ?? DEFAULT_ANNOUNCE_INTERVAL_MS;
186
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
187
+ const signal = options.signal;
188
+ return new Promise((resolve, reject) => {
189
+ if (typeof window === 'undefined') {
190
+ reject(new MinionryError('TIMEOUT', 'postMessage handshake requires a browser window (no window in this environment)'));
191
+ return;
192
+ }
193
+ if (signal?.aborted) {
194
+ reject(new MinionryError('TIMEOUT', 'postMessage handshake aborted before it started'));
195
+ return;
196
+ }
197
+ const parent = window.parent;
198
+ let settled = false;
199
+ let adopted = null;
200
+ let announceTimer;
201
+ let deadlineTimer;
202
+ let finalizeTimer;
203
+ const stopWaiting = () => {
204
+ window.removeEventListener('message', onWindowMessage);
205
+ signal?.removeEventListener('abort', onAbort);
206
+ if (announceTimer !== undefined)
207
+ clearInterval(announceTimer);
208
+ if (deadlineTimer !== undefined)
209
+ clearTimeout(deadlineTimer);
210
+ };
211
+ const onAbort = () => {
212
+ if (settled)
213
+ return;
214
+ settled = true;
215
+ stopWaiting();
216
+ if (finalizeTimer !== undefined)
217
+ clearTimeout(finalizeTimer);
218
+ if (adopted !== null) {
219
+ adopted.port.close();
220
+ adopted = null;
221
+ }
222
+ reject(new MinionryError('TIMEOUT', 'postMessage handshake aborted by the caller before a host session was established'));
223
+ };
224
+ const announce = () => {
225
+ const hello = { type: SDK_HELLO_TYPE, sdkVersion: SDK_PROTOCOL_VERSION };
226
+ for (const targetOrigin of hostOrigins ?? ['*']) {
227
+ try {
228
+ parent.postMessage(hello, targetOrigin);
229
+ }
230
+ catch {
231
+ }
232
+ }
233
+ };
234
+ const finalize = () => {
235
+ finalizeTimer = undefined;
236
+ if (settled || adopted === null)
237
+ return;
238
+ settled = true;
239
+ stopWaiting();
240
+ const { nonce, port } = adopted;
241
+ adopted = null;
242
+ const ack = { type: SDK_CONNECT_ACK_TYPE, nonce };
243
+ try {
244
+ port.postMessage(ack);
245
+ }
246
+ catch (cause) {
247
+ port.close();
248
+ reject(new MinionryError('SPACE_DISCONNECTED', `postMessage handshake failed: could not ack the host offer (${String(cause)})`));
249
+ return;
250
+ }
251
+ resolve(createPortTransport(port, requestTimeoutMs));
252
+ };
253
+ const onWindowMessage = (event) => {
254
+ if (settled)
255
+ return;
256
+ if (!isHostConnectFrame(event.data))
257
+ return;
258
+ if (event.source !== parent)
259
+ return;
260
+ if (hostOrigins && !hostOrigins.includes(event.origin))
261
+ return;
262
+ if (event.ports.length !== 1)
263
+ return;
264
+ if (adopted !== null)
265
+ adopted.port.close();
266
+ adopted = { nonce: event.data.nonce, port: event.ports[0] };
267
+ if (announceTimer !== undefined) {
268
+ clearInterval(announceTimer);
269
+ announceTimer = undefined;
270
+ }
271
+ finalizeTimer ??= setTimeout(finalize, 0);
272
+ };
273
+ window.addEventListener('message', onWindowMessage);
274
+ signal?.addEventListener('abort', onAbort);
275
+ if (timeoutMs > 0) {
276
+ deadlineTimer = setTimeout(() => {
277
+ if (settled || adopted !== null)
278
+ return;
279
+ settled = true;
280
+ stopWaiting();
281
+ if (finalizeTimer !== undefined)
282
+ clearTimeout(finalizeTimer);
283
+ reject(new MinionryError('TIMEOUT', `postMessage handshake timed out after ${timeoutMs}ms — no Minionry host answered; is this page running inside a Minionry app frame?`));
284
+ }, timeoutMs);
285
+ }
286
+ announce();
287
+ announceTimer = setInterval(announce, announceIntervalMs);
288
+ });
289
+ }
290
+ export async function connectPostMessage(options = {}) {
291
+ const provider = createProvider(await createPostMessageTransport(options));
292
+ window.minionry = provider;
293
+ return provider;
294
+ }