@mindstudio-ai/agent 0.1.107 → 0.1.109

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/dist/index.js CHANGED
@@ -102,9 +102,12 @@ function runWithContext(ctx, fn) {
102
102
  }
103
103
 
104
104
  // src/db-ws.ts
105
- var WS_REQUEST_TIMEOUT_MS = 3e5;
105
+ var DB_WS_SUBPROTOCOL = "ms-db-v2";
106
+ var WS_ACK_TIMEOUT_MS = 2e4;
107
+ var WS_IDLE_TIMEOUT_MS = 3e5;
106
108
  var WS_OPEN_TIMEOUT_MS = 5e3;
107
109
  var WS_OPEN = 1;
110
+ var WS_CHUNK_CHARS = 256 * 1024;
108
111
  var WS_MAX_FRAME_CHARS = 9e5;
109
112
  var DbWsTransportError = class extends Error {
110
113
  constructor(message, sent = false) {
@@ -117,13 +120,86 @@ function isReadOnlySql(sql) {
117
120
  const head = sql.trimStart().slice(0, 8).toUpperCase();
118
121
  return head.startsWith("SELECT") || head.startsWith("PRAGMA") || head.startsWith("EXPLAIN");
119
122
  }
120
- var socket = null;
123
+ var conn = null;
121
124
  var opening = null;
122
125
  var idCounter = 0;
123
- var pending = /* @__PURE__ */ new Map();
124
- function getOrOpen(url) {
125
- if (socket && socket.readyState === WS_OPEN) {
126
- return Promise.resolve(socket);
126
+ function clearPendingTimer(p) {
127
+ if (p.timer !== null) {
128
+ clearTimeout(p.timer);
129
+ p.timer = null;
130
+ }
131
+ }
132
+ function forget(self, id) {
133
+ self.pending.delete(id);
134
+ self.inbound.delete(id);
135
+ if (self.retired && self.pending.size === 0) {
136
+ try {
137
+ self.ws.close();
138
+ } catch {
139
+ }
140
+ }
141
+ }
142
+ function retire(self, reason) {
143
+ if (self.retired) {
144
+ return;
145
+ }
146
+ self.retired = true;
147
+ if (conn === self) {
148
+ conn = null;
149
+ }
150
+ try {
151
+ console.warn(
152
+ `[mindstudio] db: retiring WebSocket after ${reason}; next query reconnects.`
153
+ );
154
+ } catch {
155
+ }
156
+ if (self.pending.size === 0) {
157
+ try {
158
+ self.ws.close();
159
+ } catch {
160
+ }
161
+ }
162
+ }
163
+ function armIdleTimer(self, id, p) {
164
+ clearPendingTimer(p);
165
+ p.timer = setTimeout(() => {
166
+ forget(self, id);
167
+ retire(self, "a reply went silent");
168
+ p.reject(new DbWsTransportError("ws reply went silent", true));
169
+ }, WS_IDLE_TIMEOUT_MS);
170
+ }
171
+ function deliver(self, msg) {
172
+ const p = self.pending.get(msg.id);
173
+ if (!p) {
174
+ return;
175
+ }
176
+ clearPendingTimer(p);
177
+ forget(self, msg.id);
178
+ if (msg.error) {
179
+ p.reject(
180
+ new MindStudioError(
181
+ `[db] ${msg.error.message || "Database query failed"}`,
182
+ msg.error.code || "db_query_error",
183
+ 400,
184
+ msg.error
185
+ )
186
+ );
187
+ } else {
188
+ p.resolve(msg.results);
189
+ }
190
+ }
191
+ function failTransport(self, id, err) {
192
+ const p = self.pending.get(id);
193
+ if (!p) {
194
+ return;
195
+ }
196
+ clearPendingTimer(p);
197
+ forget(self, id);
198
+ p.reject(err);
199
+ }
200
+ function getOrOpen(url, token) {
201
+ if (conn && !conn.retired && conn.ws.readyState === WS_OPEN) {
202
+ return Promise.resolve(conn);
127
203
  }
128
204
  if (opening) {
129
205
  return opening;
@@ -136,17 +212,22 @@ function getOrOpen(url) {
136
212
  }
137
213
  opening = new Promise((resolve, reject) => {
138
214
  let ws;
215
+ let self = null;
139
216
  const fail = (err) => {
140
217
  clearTimeout(openTimer);
141
- if (socket === ws) {
142
- socket = null;
218
+ if (self) {
219
+ self.retired = true;
220
+ if (conn === self) {
221
+ conn = null;
222
+ }
223
+ for (const [, p] of self.pending) {
224
+ clearPendingTimer(p);
225
+ p.reject(new DbWsTransportError(err.message, true));
226
+ }
227
+ self.pending.clear();
228
+ self.inbound.clear();
143
229
  }
144
230
  opening = null;
145
- for (const [, p] of pending) {
146
- clearTimeout(p.timer);
147
- p.reject(new DbWsTransportError(err.message, true));
148
- }
149
- pending.clear();
150
231
  reject(err);
151
232
  };
152
233
  const openTimer = setTimeout(() => {
@@ -157,16 +238,26 @@ function getOrOpen(url) {
157
238
  fail(new DbWsTransportError("ws open timed out"));
158
239
  }, WS_OPEN_TIMEOUT_MS);
159
240
  try {
160
- ws = new WebSocketCtor(url);
241
+ ws = new WebSocketCtor(url, ["auth", token, DB_WS_SUBPROTOCOL]);
161
242
  } catch (err) {
162
243
  fail(new DbWsTransportError(err?.message || "ws construct failed"));
163
244
  return;
164
245
  }
246
+ self = {
247
+ ws,
248
+ pending: /* @__PURE__ */ new Map(),
249
+ chunked: false,
250
+ inbound: /* @__PURE__ */ new Map(),
251
+ acksSeen: false,
252
+ retired: false
253
+ };
254
+ const c = self;
165
255
  ws.onopen = () => {
166
256
  clearTimeout(openTimer);
167
- socket = ws;
257
+ c.chunked = ws.protocol === DB_WS_SUBPROTOCOL;
258
+ conn = c;
168
259
  opening = null;
169
- resolve(ws);
260
+ resolve(c);
170
261
  };
171
262
  ws.onmessage = (ev) => {
172
263
  let msg;
@@ -177,26 +268,54 @@ function getOrOpen(url) {
177
268
  } catch {
178
269
  return;
179
270
  }
180
- if (!msg || msg.t !== "db-result" || msg.id == null) {
271
+ if (!msg || msg.id == null) {
181
272
  return;
182
273
  }
183
- const p = pending.get(msg.id);
184
- if (!p) {
274
+ if (msg.t === "db-ack") {
275
+ c.acksSeen = true;
276
+ const p = c.pending.get(msg.id);
277
+ if (p && !p.acked) {
278
+ p.acked = true;
279
+ armIdleTimer(c, msg.id, p);
280
+ }
185
281
  return;
186
282
  }
187
- clearTimeout(p.timer);
188
- pending.delete(msg.id);
189
- if (msg.error) {
190
- p.reject(
191
- new MindStudioError(
192
- `[db] ${msg.error.message || "Database query failed"}`,
193
- msg.error.code || "db_query_error",
194
- 400,
195
- msg.error
196
- )
197
- );
198
- } else {
199
- p.resolve(msg.results);
283
+ if (msg.t === "db-chunk") {
284
+ const p = c.pending.get(msg.id);
285
+ if (!p || typeof msg.data !== "string") {
286
+ return;
287
+ }
288
+ const parts = c.inbound.get(msg.id);
289
+ if (parts) {
290
+ parts.push(msg.data);
291
+ } else {
292
+ c.inbound.set(msg.id, [msg.data]);
293
+ }
294
+ armIdleTimer(c, msg.id, p);
295
+ return;
296
+ }
297
+ if (msg.t === "db-end") {
298
+ const parts = c.inbound.get(msg.id);
299
+ if (!parts) {
300
+ return;
301
+ }
302
+ c.inbound.delete(msg.id);
303
+ let assembled;
304
+ try {
305
+ assembled = JSON.parse(parts.join(""));
306
+ } catch {
307
+ failTransport(
308
+ c,
309
+ msg.id,
310
+ new DbWsTransportError("chunked reply did not parse", true)
311
+ );
312
+ return;
313
+ }
314
+ deliver(c, assembled);
315
+ return;
316
+ }
317
+ if (msg.t === "db-result") {
318
+ deliver(c, msg);
200
319
  }
201
320
  };
202
321
  ws.onerror = () => fail(new DbWsTransportError("ws error"));
@@ -204,28 +323,59 @@ function getOrOpen(url) {
204
323
  });
205
324
  return opening;
206
325
  }
326
+ function* outboundFrames(id, json) {
327
+ if (json.length <= WS_CHUNK_CHARS) {
328
+ yield json;
329
+ return;
330
+ }
331
+ let seq = 0;
332
+ for (let offset = 0; offset < json.length; offset += WS_CHUNK_CHARS) {
333
+ yield JSON.stringify({
334
+ t: "db-chunk",
335
+ id,
336
+ seq: seq++,
337
+ data: json.slice(offset, offset + WS_CHUNK_CHARS)
338
+ });
339
+ }
340
+ yield JSON.stringify({ t: "db-end", id });
341
+ }
207
342
  async function executeDbBatchOverWs(url, token, databaseId, queries) {
208
343
  const id = ++idCounter;
209
344
  const frame = JSON.stringify({ t: "db", id, token, databaseId, queries });
210
- if (frame.length > WS_MAX_FRAME_CHARS) {
345
+ const self = await getOrOpen(url, token);
346
+ if (!self.chunked && frame.length > WS_MAX_FRAME_CHARS) {
211
347
  throw new DbWsTransportError("payload too large for ws");
212
348
  }
213
- const ws = await getOrOpen(url);
214
349
  return await new Promise((resolve, reject) => {
215
- const timer = setTimeout(() => {
216
- pending.delete(id);
217
- reject(new DbWsTransportError("ws execute timed out", true));
218
- }, WS_REQUEST_TIMEOUT_MS);
219
- pending.set(id, { resolve, reject, timer });
350
+ const pending = { resolve, reject, timer: null, acked: false };
351
+ if (self.acksSeen) {
352
+ pending.timer = setTimeout(() => {
353
+ forget(self, id);
354
+ retire(self, "a request went unacknowledged");
355
+ reject(new DbWsTransportError("ws ack timed out", true));
356
+ }, WS_ACK_TIMEOUT_MS);
357
+ } else {
358
+ armIdleTimer(self, id, pending);
359
+ }
360
+ self.pending.set(id, pending);
361
+ let started = false;
362
+ let complete = false;
220
363
  try {
221
- ws.send(frame);
364
+ for (const out of outboundFrames(id, frame)) {
365
+ self.ws.send(out);
366
+ started = true;
367
+ }
368
+ complete = true;
222
369
  } catch (err) {
223
- clearTimeout(timer);
224
- pending.delete(id);
370
+ clearPendingTimer(pending);
371
+ forget(self, id);
372
+ if (started) {
373
+ retire(self, "a partial request upload failed");
374
+ }
225
375
  reject(
226
376
  new DbWsTransportError(
227
377
  `ws send failed: ${err?.message || "unknown"}`,
228
- false
378
+ complete
229
379
  )
230
380
  );
231
381
  }
@@ -2442,6 +2592,7 @@ function toBase64(content) {
2442
2592
  typeof content === "string" ? Buffer.from(content) : content
2443
2593
  ).toString("base64");
2444
2594
  }
2595
+ var PRESIGN_THRESHOLD = 4 * 1024 * 1024;
2445
2596
  function extensionFor(filename) {
2446
2597
  if (!filename) {
2447
2598
  return "";
@@ -2469,16 +2620,73 @@ var Store = class {
2469
2620
  const ext = extensionFor(options?.filename);
2470
2621
  const key = options?.key ?? (options?.contentAddressed ? `${createHash("sha256").update(content).digest("hex")}${ext}` : `${randomUUID()}${ext}`);
2471
2622
  const cacheControl = options?.cacheControl ?? (!options?.key && this._access === "public" ? "public, max-age=31536000, immutable" : void 0);
2623
+ const bytes = typeof content === "string" ? Buffer.from(content) : content;
2624
+ if (bytes.byteLength >= PRESIGN_THRESHOLD) {
2625
+ return this._putPresigned(bytes, key, {
2626
+ ...options?.contentType ? { contentType: options.contentType } : {},
2627
+ ...cacheControl ? { cacheControl } : {}
2628
+ });
2629
+ }
2472
2630
  const meta = await this._call("put", {
2473
2631
  store: this._store,
2474
2632
  access: this._access,
2475
2633
  key,
2476
- body: toBase64(content),
2634
+ body: toBase64(bytes),
2477
2635
  ...options?.contentType ? { contentType: options.contentType } : {},
2478
2636
  ...cacheControl ? { cacheControl } : {}
2479
2637
  });
2480
2638
  return this._toFile(key, meta);
2481
2639
  }
2640
+ /**
2641
+ * Large-content half of {@link put}: mint a presigned POST scoped to exactly
2642
+ * these bytes and send them direct to storage — the JSON transport's body
2643
+ * limit never sees them. Same result contract as the small path; the final
2644
+ * `head` keeps the returned metadata honest (server-observed size/type/time).
2645
+ */
2646
+ async _putPresigned(bytes, key, options) {
2647
+ const res = await this._call("create-upload", {
2648
+ store: this._store,
2649
+ access: this._access,
2650
+ key,
2651
+ maxSize: bytes.byteLength,
2652
+ ...options.contentType ? { contentType: options.contentType } : {},
2653
+ ...options.cacheControl ? { cacheControl: options.cacheControl } : {}
2654
+ });
2655
+ const form = new FormData();
2656
+ for (const [k, v] of Object.entries(
2657
+ res.uploadFields
2658
+ )) {
2659
+ form.append(k, v);
2660
+ }
2661
+ const view = new Uint8Array(
2662
+ bytes.buffer,
2663
+ bytes.byteOffset,
2664
+ bytes.byteLength
2665
+ );
2666
+ form.append(
2667
+ "file",
2668
+ new Blob([view], options.contentType ? { type: options.contentType } : void 0)
2669
+ );
2670
+ const upload = await fetch(res.uploadUrl, {
2671
+ method: "POST",
2672
+ body: form
2673
+ });
2674
+ if (!upload.ok) {
2675
+ const errorText = await upload.text().catch(() => "");
2676
+ throw new MindStudioError(
2677
+ `Upload failed: ${upload.status} ${upload.statusText}${errorText ? ` \u2014 ${errorText.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200)}` : ""}`,
2678
+ "upload_error",
2679
+ upload.status,
2680
+ errorText || void 0
2681
+ );
2682
+ }
2683
+ const meta = await this._call("head", {
2684
+ store: this._store,
2685
+ access: this._access,
2686
+ key
2687
+ });
2688
+ return this._toFile(key, meta);
2689
+ }
2482
2690
  /** Read an object's bytes (backend / trusted context). */
2483
2691
  async get(key) {
2484
2692
  const res = await this._call("get", {
@@ -2814,6 +3022,50 @@ function createAnalytics(call) {
2814
3022
  };
2815
3023
  }
2816
3024
 
3025
+ // src/email/index.ts
3026
+ var iso = (v) => v === void 0 ? void 0 : v instanceof Date ? v.toISOString() : v;
3027
+ function createEmail(call) {
3028
+ return {
3029
+ stats(options) {
3030
+ return call("stats", {
3031
+ start: iso(options?.start),
3032
+ end: iso(options?.end),
3033
+ buckets: options?.buckets
3034
+ });
3035
+ },
3036
+ messages(options) {
3037
+ return call("messages", {
3038
+ ...options,
3039
+ start: iso(options?.start),
3040
+ end: iso(options?.end)
3041
+ });
3042
+ },
3043
+ message(messageId) {
3044
+ return call("message", { messageId });
3045
+ },
3046
+ batches(options) {
3047
+ return call("batches", {
3048
+ start: iso(options?.start),
3049
+ end: iso(options?.end),
3050
+ limit: options?.limit,
3051
+ offset: options?.offset
3052
+ });
3053
+ },
3054
+ batch(batchId) {
3055
+ return call("batch", { batchId });
3056
+ },
3057
+ suppressions(options) {
3058
+ return call("suppressions", options ?? {});
3059
+ },
3060
+ suppress(email2) {
3061
+ return call("suppress", { email: email2 });
3062
+ },
3063
+ unsuppress(email2) {
3064
+ return call("unsuppress", { email: email2 });
3065
+ }
3066
+ };
3067
+ }
3068
+
2817
3069
  // src/task/index.ts
2818
3070
  import { randomUUID as randomUUID2 } from "crypto";
2819
3071
 
@@ -5526,6 +5778,7 @@ var MindStudioAgent = class {
5526
5778
  _dataSources;
5527
5779
  _voice;
5528
5780
  _analytics;
5781
+ _email;
5529
5782
  /** @internal Auth type — 'internal' for CALLBACK_TOKEN (managed mode), 'apiKey' otherwise. */
5530
5783
  _authType;
5531
5784
  /** @internal Usage source sent on step executions (from MINDSTUDIO_REQUEST_SOURCE).
@@ -6570,6 +6823,24 @@ var MindStudioAgent = class {
6570
6823
  this._analyticsRequest.bind(this)
6571
6824
  );
6572
6825
  }
6826
+ /**
6827
+ * Outbound email: the per-recipient delivery log, blast stats, and this app's
6828
+ * unsubscribe list. Sending is the `sendEmail` action, not this namespace.
6829
+ *
6830
+ * @example
6831
+ * ```ts
6832
+ * import { email } from '@mindstudio-ai/agent';
6833
+ *
6834
+ * // A support lookup: did this person get their mail?
6835
+ * const { messages } = await email.messages({ recipient: 'a@b.com' });
6836
+ *
6837
+ * // How did the August newsletter do?
6838
+ * const blast = await email.batch('newsletter-2026-08');
6839
+ * ```
6840
+ */
6841
+ get email() {
6842
+ return this._email ??= createEmail(this._emailRequest.bind(this));
6843
+ }
6573
6844
  /**
6574
6845
  * Jewel surfaces: arrival-shaped triggers (`propose`) and the app-native
6575
6846
  * approval queue (`queue.list` / `queue.resolve`). See {@link JewelsApi}.
@@ -6708,6 +6979,18 @@ var MindStudioAgent = class {
6708
6979
  * of surfacing as empty data. Deliberately single-shot and scoped to this
6709
6980
  * namespace — the other brokered transports keep their semantics.
6710
6981
  */
6982
+ /**
6983
+ * Brokered email transport. No 429 retry, unlike analytics: this namespace
6984
+ * carries writes (suppress / unsuppress), and silently retrying a mutation is a
6985
+ * different risk from retrying an idempotent read. A caller that wants one can
6986
+ * retry a read itself.
6987
+ */
6988
+ async _emailRequest(op, body) {
6989
+ return this._brokeredRequest("email", op, body, {
6990
+ fallbackMessage: "Email API call failed",
6991
+ fallbackCode: "email_error"
6992
+ });
6993
+ }
6711
6994
  async _analyticsRequest(op, body) {
6712
6995
  const call = () => this._brokeredRequest("analytics", op, body, {
6713
6996
  fallbackMessage: "Analytics read failed",
@@ -7612,6 +7895,13 @@ var analytics = new Proxy({}, {
7612
7895
  return typeof value === "function" ? value.bind(target) : value;
7613
7896
  }
7614
7897
  });
7898
+ var email = new Proxy({}, {
7899
+ get(_, prop) {
7900
+ const target = mindstudio.email;
7901
+ const value = Reflect.get(target, prop, target);
7902
+ return typeof value === "function" ? value.bind(target) : value;
7903
+ }
7904
+ });
7615
7905
  var session = new Proxy(
7616
7906
  {},
7617
7907
  {
@@ -7640,6 +7930,7 @@ export {
7640
7930
  db,
7641
7931
  index_default as default,
7642
7932
  defineJewel,
7933
+ email,
7643
7934
  files,
7644
7935
  getRequestContext,
7645
7936
  mindstudio,