@mindstudio-ai/agent 0.1.35 → 0.1.37

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
@@ -7,6 +7,18 @@ var MindStudioError = class extends Error {
7
7
  this.details = details;
8
8
  }
9
9
  name = "MindStudioError";
10
+ toString() {
11
+ return `MindStudioError [${this.code}] (${this.status}): ${this.message}`;
12
+ }
13
+ toJSON() {
14
+ return {
15
+ name: this.name,
16
+ message: this.message,
17
+ code: this.code,
18
+ status: this.status,
19
+ ...this.details != null && { details: this.details }
20
+ };
21
+ }
10
22
  };
11
23
 
12
24
  // src/http.ts
@@ -45,11 +57,17 @@ async function requestWithRetry(config, method, url, body, attempt) {
45
57
  try {
46
58
  const body2 = JSON.parse(text);
47
59
  details = body2;
48
- const errMsg = body2.error ?? body2.message ?? body2.details;
60
+ const errMsg = (typeof body2.error === "string" ? body2.error : void 0) ?? (typeof body2.message === "string" ? body2.message : void 0) ?? (typeof body2.details === "string" ? body2.details : void 0);
49
61
  if (errMsg) message = errMsg;
62
+ else if (body2.error || body2.message || body2.details) {
63
+ message = JSON.stringify(body2.error ?? body2.message ?? body2.details);
64
+ }
50
65
  if (body2.code) code = body2.code;
51
66
  } catch {
52
- if (text && text.length < 500) message = text;
67
+ if (text) {
68
+ const stripped = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
69
+ if (stripped) message = stripped.slice(0, 200);
70
+ }
53
71
  }
54
72
  } catch {
55
73
  }
@@ -82,7 +100,7 @@ var RateLimiter = class {
82
100
  async acquire() {
83
101
  if (this.callCount >= this.callCap) {
84
102
  throw new MindStudioError(
85
- `Call cap reached (${this.callCap} calls). Internal tokens are limited to 500 calls per execution.`,
103
+ `Call cap exceeded (${this.callCap} calls per execution). Reduce the number of API calls or use executeStepBatch() to combine multiple steps.`,
86
104
  "call_cap_exceeded",
87
105
  429
88
106
  );
@@ -149,7 +167,7 @@ function loadConfig() {
149
167
 
150
168
  // src/auth/index.ts
151
169
  var AuthContext = class {
152
- /** The current user's ID. */
170
+ /** The current user's ID, or null for unauthenticated users. */
153
171
  userId;
154
172
  /** The current user's roles in this app. */
155
173
  roles;
@@ -188,9 +206,16 @@ var AuthContext = class {
188
206
  * ```
189
207
  */
190
208
  requireRole(...roles) {
209
+ if (this.userId == null) {
210
+ throw new MindStudioError(
211
+ "No authenticated user",
212
+ "unauthenticated",
213
+ 401
214
+ );
215
+ }
191
216
  if (!this.hasRole(...roles)) {
192
217
  throw new MindStudioError(
193
- `User does not have required role: ${roles.join(", ")}`,
218
+ `User has role(s) [${this.roles.join(", ") || "none"}] but requires one of: [${roles.join(", ")}]`,
194
219
  "forbidden",
195
220
  403
196
221
  );
@@ -244,6 +269,7 @@ function escapeValue(val) {
244
269
  }
245
270
  var USER_PREFIX = "@@user@@";
246
271
  function deserializeRow(row, columns) {
272
+ if (row == null) return row;
247
273
  const result = {};
248
274
  for (const [key, value] of Object.entries(row)) {
249
275
  const col = columns.find((c) => c.name === key);
@@ -273,11 +299,6 @@ function buildSelect(table, options = {}) {
273
299
  if (options.offset != null) sql += ` OFFSET ${options.offset}`;
274
300
  return { sql, params: params.length > 0 ? params : void 0 };
275
301
  }
276
- function buildCount(table, where, whereParams) {
277
- let sql = `SELECT COUNT(*) as count FROM ${table}`;
278
- if (where) sql += ` WHERE ${where}`;
279
- return { sql, params: whereParams?.length ? whereParams : void 0 };
280
- }
281
302
  function buildExists(table, where, whereParams, negate) {
282
303
  const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
283
304
  const fn = negate ? "NOT EXISTS" : "EXISTS";
@@ -348,20 +369,20 @@ function compilePredicate(fn) {
348
369
  try {
349
370
  const source = fn.toString();
350
371
  const paramName = extractParamName(source);
351
- if (!paramName) return { type: "js", fn };
372
+ if (!paramName) return { type: "js", fn, reason: "could not extract parameter name" };
352
373
  const body = extractBody(source);
353
- if (!body) return { type: "js", fn };
374
+ if (!body) return { type: "js", fn, reason: "could not extract function body" };
354
375
  const tokens = tokenize(body);
355
- if (tokens.length === 0) return { type: "js", fn };
376
+ if (tokens.length === 0) return { type: "js", fn, reason: "empty token stream" };
356
377
  const parser = new Parser(tokens, paramName, fn);
357
378
  const ast = parser.parseExpression();
358
- if (!ast) return { type: "js", fn };
359
- if (parser.pos < tokens.length) return { type: "js", fn };
379
+ if (!ast) return { type: "js", fn, reason: "could not parse expression" };
380
+ if (parser.pos < tokens.length) return { type: "js", fn, reason: "unexpected tokens after expression" };
360
381
  const where = compileNode(ast);
361
- if (!where) return { type: "js", fn };
382
+ if (!where) return { type: "js", fn, reason: "could not compile to SQL" };
362
383
  return { type: "sql", where };
363
- } catch {
364
- return { type: "js", fn };
384
+ } catch (err) {
385
+ return { type: "js", fn, reason: `compilation error: ${err?.message || "unknown"}` };
365
386
  }
366
387
  }
367
388
  function extractParamName(source) {
@@ -838,6 +859,11 @@ var Query = class _Query {
838
859
  _limit;
839
860
  _offset;
840
861
  _config;
862
+ /** @internal Pre-compiled WHERE clause (bypasses predicate compiler). Used by Table.get(). */
863
+ _rawWhere;
864
+ _rawWhereParams;
865
+ /** @internal Post-process transform applied after row deserialization. */
866
+ _postProcess;
841
867
  constructor(config, options) {
842
868
  this._config = config;
843
869
  this._predicates = options?.predicates ?? [];
@@ -845,6 +871,9 @@ var Query = class _Query {
845
871
  this._reversed = options?.reversed ?? false;
846
872
  this._limit = options?.limit;
847
873
  this._offset = options?.offset;
874
+ this._postProcess = options?.postProcess;
875
+ this._rawWhere = options?.rawWhere;
876
+ this._rawWhereParams = options?.rawWhereParams;
848
877
  }
849
878
  _clone(overrides) {
850
879
  return new _Query(this._config, {
@@ -852,7 +881,10 @@ var Query = class _Query {
852
881
  sortAccessor: overrides.sortAccessor ?? this._sortAccessor,
853
882
  reversed: overrides.reversed ?? this._reversed,
854
883
  limit: overrides.limit ?? this._limit,
855
- offset: overrides.offset ?? this._offset
884
+ offset: overrides.offset ?? this._offset,
885
+ postProcess: overrides.postProcess,
886
+ rawWhere: this._rawWhere,
887
+ rawWhereParams: this._rawWhereParams
856
888
  });
857
889
  }
858
890
  // -------------------------------------------------------------------------
@@ -876,41 +908,29 @@ var Query = class _Query {
876
908
  // -------------------------------------------------------------------------
877
909
  // Terminal methods
878
910
  // -------------------------------------------------------------------------
879
- async first() {
880
- const rows = await this._clone({ limit: 1 })._execute();
881
- return rows[0] ?? null;
911
+ first() {
912
+ return this._clone({
913
+ limit: 1,
914
+ postProcess: (rows) => rows[0] ?? null
915
+ });
882
916
  }
883
- async last() {
884
- const rows = await this._clone({ limit: 1, reversed: !this._reversed })._execute();
885
- return rows[0] ?? null;
917
+ last() {
918
+ return this._clone({
919
+ limit: 1,
920
+ reversed: !this._reversed,
921
+ postProcess: (rows) => rows[0] ?? null
922
+ });
886
923
  }
887
- async count() {
888
- const compiled = this._compilePredicates();
889
- if (compiled.allSql) {
890
- const query = buildCount(
891
- this._config.tableName,
892
- compiled.sqlWhere || void 0
893
- );
894
- const results = await this._config.executeBatch([query]);
895
- const row = results[0]?.rows[0];
896
- return row?.count ?? 0;
897
- }
898
- const rows = await this._fetchAndFilterInJs(compiled);
899
- return rows.length;
924
+ count() {
925
+ return this._clone({
926
+ postProcess: (rows) => rows.length
927
+ });
900
928
  }
901
- async some() {
902
- const compiled = this._compilePredicates();
903
- if (compiled.allSql) {
904
- const query = buildExists(
905
- this._config.tableName,
906
- compiled.sqlWhere || void 0
907
- );
908
- const results = await this._config.executeBatch([query]);
909
- const row = results[0]?.rows[0];
910
- return row?.result === 1;
911
- }
912
- const rows = await this._fetchAndFilterInJs(compiled);
913
- return rows.length > 0;
929
+ some() {
930
+ return this._clone({
931
+ limit: 1,
932
+ postProcess: (rows) => rows.length > 0
933
+ });
914
934
  }
915
935
  async every() {
916
936
  const compiled = this._compilePredicates();
@@ -931,25 +951,25 @@ var Query = class _Query {
931
951
  (row) => this._predicates.every((pred) => pred(row))
932
952
  );
933
953
  }
934
- async min(accessor) {
954
+ min(accessor) {
935
955
  return this.sortBy(accessor).first();
936
956
  }
937
- async max(accessor) {
957
+ max(accessor) {
938
958
  return this.sortBy(accessor).reverse().first();
939
959
  }
940
- async groupBy(accessor) {
941
- const rows = await this._execute();
942
- const map = /* @__PURE__ */ new Map();
943
- for (const row of rows) {
944
- const key = accessor(row);
945
- const group = map.get(key);
946
- if (group) {
947
- group.push(row);
948
- } else {
949
- map.set(key, [row]);
960
+ groupBy(accessor) {
961
+ return this._clone({
962
+ postProcess: (rows) => {
963
+ const map = /* @__PURE__ */ new Map();
964
+ for (const row of rows) {
965
+ const key = accessor(row);
966
+ const group = map.get(key);
967
+ if (group) group.push(row);
968
+ else map.set(key, [row]);
969
+ }
970
+ return map;
950
971
  }
951
- }
952
- return map;
972
+ });
953
973
  }
954
974
  // -------------------------------------------------------------------------
955
975
  // Batch compilation — used by db.batch() to extract SQL without executing
@@ -963,6 +983,16 @@ var Query = class _Query {
963
983
  * all rows and this query can filter them in JS post-fetch.
964
984
  */
965
985
  _compile() {
986
+ if (this._rawWhere) {
987
+ const query = buildSelect(this._config.tableName, {
988
+ where: this._rawWhere,
989
+ whereParams: this._rawWhereParams,
990
+ orderBy: void 0,
991
+ limit: this._limit,
992
+ offset: this._offset
993
+ });
994
+ return { type: "query", query, fallbackQuery: null, config: this._config, postProcess: this._postProcess };
995
+ }
966
996
  const compiled = this._compilePredicates();
967
997
  const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
968
998
  if (compiled.allSql) {
@@ -973,7 +1003,7 @@ var Query = class _Query {
973
1003
  limit: this._limit,
974
1004
  offset: this._offset
975
1005
  });
976
- return { type: "query", query, fallbackQuery: null, config: this._config };
1006
+ return { type: "query", query, fallbackQuery: null, config: this._config, postProcess: this._postProcess };
977
1007
  }
978
1008
  const fallbackQuery = buildSelect(this._config.tableName);
979
1009
  return {
@@ -985,7 +1015,8 @@ var Query = class _Query {
985
1015
  sortAccessor: this._sortAccessor,
986
1016
  reversed: this._reversed,
987
1017
  limit: this._limit,
988
- offset: this._offset
1018
+ offset: this._offset,
1019
+ postProcess: this._postProcess
989
1020
  };
990
1021
  }
991
1022
  /**
@@ -1002,7 +1033,9 @@ var Query = class _Query {
1002
1033
  compiled.config.columns
1003
1034
  )
1004
1035
  );
1005
- if (compiled.query) return rows;
1036
+ if (compiled.query) {
1037
+ return compiled.postProcess ? compiled.postProcess(rows) : rows;
1038
+ }
1006
1039
  let filtered = compiled.predicates ? rows.filter((row) => compiled.predicates.every((pred) => pred(row))) : rows;
1007
1040
  if (compiled.sortAccessor) {
1008
1041
  const accessor = compiled.sortAccessor;
@@ -1020,21 +1053,39 @@ var Query = class _Query {
1020
1053
  const end = compiled.limit != null ? start + compiled.limit : void 0;
1021
1054
  filtered = filtered.slice(start, end);
1022
1055
  }
1023
- return filtered;
1056
+ return compiled.postProcess ? compiled.postProcess(filtered) : filtered;
1024
1057
  }
1025
1058
  // -------------------------------------------------------------------------
1026
1059
  // PromiseLike
1027
1060
  // -------------------------------------------------------------------------
1028
1061
  then(onfulfilled, onrejected) {
1029
- return this._execute().then(onfulfilled, onrejected);
1062
+ const promise = this._execute().then(
1063
+ (rows) => this._postProcess ? this._postProcess(rows) : rows
1064
+ );
1065
+ return promise.then(onfulfilled, onrejected);
1030
1066
  }
1031
1067
  catch(onrejected) {
1032
- return this._execute().catch(onrejected);
1068
+ return this.then(void 0, onrejected);
1033
1069
  }
1034
1070
  // -------------------------------------------------------------------------
1035
1071
  // Execution internals
1036
1072
  // -------------------------------------------------------------------------
1037
1073
  async _execute() {
1074
+ if (this._rawWhere) {
1075
+ const query = buildSelect(this._config.tableName, {
1076
+ where: this._rawWhere,
1077
+ whereParams: this._rawWhereParams,
1078
+ limit: this._limit,
1079
+ offset: this._offset
1080
+ });
1081
+ const results = await this._config.executeBatch([query]);
1082
+ return results[0].rows.map(
1083
+ (row) => deserializeRow(
1084
+ row,
1085
+ this._config.columns
1086
+ )
1087
+ );
1088
+ }
1038
1089
  const compiled = this._compilePredicates();
1039
1090
  if (compiled.allSql) {
1040
1091
  const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
@@ -1085,9 +1136,12 @@ var Query = class _Query {
1085
1136
  }
1086
1137
  async _fetchAndFilterInJs(compiled) {
1087
1138
  const allRows = await this._fetchAllRows();
1088
- if (compiled.compiled.some((c) => c.type === "js")) {
1139
+ const jsFallbacks = compiled.compiled.filter((c) => c.type === "js");
1140
+ if (jsFallbacks.length > 0) {
1141
+ const reasons = jsFallbacks.map((c) => c.type === "js" ? c.reason : void 0).filter(Boolean);
1142
+ const reasonSuffix = reasons.length > 0 ? ` (${reasons.join("; ")})` : "";
1089
1143
  console.warn(
1090
- `[mindstudio] Filter on ${this._config.tableName} could not be compiled to SQL \u2014 scanning ${allRows.length} rows in JS`
1144
+ `[mindstudio] Filter on '${this._config.tableName}' could not be compiled to SQL${reasonSuffix} \u2014 scanning ${allRows.length} rows in JS`
1091
1145
  );
1092
1146
  }
1093
1147
  return allRows.filter(
@@ -1160,8 +1214,10 @@ var Mutation = class _Mutation {
1160
1214
  */
1161
1215
  _compile() {
1162
1216
  if (this._executor) {
1163
- throw new Error(
1164
- "This operation cannot be batched (e.g. removeAll with a predicate that cannot compile to SQL). Await it separately."
1217
+ throw new MindStudioError(
1218
+ "This operation cannot be batched (e.g. removeAll with a JS-fallback predicate). Await it separately instead of passing to db.batch().",
1219
+ "not_batchable",
1220
+ 400
1165
1221
  );
1166
1222
  }
1167
1223
  return {
@@ -1198,58 +1254,61 @@ var Table = class {
1198
1254
  this._config = config;
1199
1255
  }
1200
1256
  // -------------------------------------------------------------------------
1201
- // Reads — direct
1257
+ // Reads — all return batchable Query objects (lazy until awaited)
1202
1258
  // -------------------------------------------------------------------------
1203
- async get(id) {
1204
- const query = buildSelect(this._config.tableName, {
1205
- where: `id = ?`,
1206
- whereParams: [id],
1207
- limit: 1
1259
+ /** Get a single row by ID. Returns null if not found. */
1260
+ get(id) {
1261
+ return new Query(this._config, {
1262
+ rawWhere: "id = ?",
1263
+ rawWhereParams: [id],
1264
+ limit: 1,
1265
+ postProcess: (rows) => rows[0] ?? null
1208
1266
  });
1209
- const results = await this._config.executeBatch([query]);
1210
- if (results[0].rows.length === 0) return null;
1211
- return deserializeRow(
1212
- results[0].rows[0],
1213
- this._config.columns
1214
- );
1215
1267
  }
1216
- async findOne(predicate) {
1268
+ /** Find the first row matching a predicate. Returns null if none match. */
1269
+ findOne(predicate) {
1217
1270
  return this.filter(predicate).first();
1218
1271
  }
1219
- async count(predicate) {
1272
+ count(predicate) {
1220
1273
  if (predicate) return this.filter(predicate).count();
1221
- const query = buildCount(this._config.tableName);
1222
- const results = await this._config.executeBatch([query]);
1223
- const row = results[0]?.rows[0];
1224
- return row?.count ?? 0;
1274
+ return this.toArray().count();
1225
1275
  }
1226
- async some(predicate) {
1276
+ /** True if any row matches the predicate. */
1277
+ some(predicate) {
1227
1278
  return this.filter(predicate).some();
1228
1279
  }
1280
+ /** True if all rows match the predicate. */
1229
1281
  async every(predicate) {
1230
1282
  return this.filter(predicate).every();
1231
1283
  }
1284
+ /** True if the table has zero rows. */
1232
1285
  async isEmpty() {
1233
1286
  const query = buildExists(this._config.tableName, void 0, void 0, true);
1234
1287
  const results = await this._config.executeBatch([query]);
1235
1288
  const row = results[0]?.rows[0];
1236
1289
  return row?.result === 1;
1237
1290
  }
1238
- async min(accessor) {
1291
+ /** Row with the minimum value for a field, or null if table is empty. */
1292
+ min(accessor) {
1239
1293
  return this.sortBy(accessor).first();
1240
1294
  }
1241
- async max(accessor) {
1295
+ /** Row with the maximum value for a field, or null if table is empty. */
1296
+ max(accessor) {
1242
1297
  return this.sortBy(accessor).reverse().first();
1243
1298
  }
1244
- async groupBy(accessor) {
1299
+ /** Group rows by a field. Returns a Map. */
1300
+ groupBy(accessor) {
1245
1301
  return new Query(this._config).groupBy(accessor);
1246
1302
  }
1247
- // -------------------------------------------------------------------------
1248
- // Reads — chainable
1249
- // -------------------------------------------------------------------------
1303
+ /** Get all rows as an array. */
1304
+ toArray() {
1305
+ return new Query(this._config);
1306
+ }
1307
+ /** Filter rows by a predicate. Returns a chainable Query. */
1250
1308
  filter(predicate) {
1251
1309
  return new Query(this._config).filter(predicate);
1252
1310
  }
1311
+ /** Sort rows by a field. Returns a chainable Query. */
1253
1312
  sortBy(accessor) {
1254
1313
  return new Query(this._config).sortBy(accessor);
1255
1314
  }
@@ -1276,7 +1335,11 @@ var Table = class {
1276
1335
  this._config.columns
1277
1336
  );
1278
1337
  }
1279
- return void 0;
1338
+ throw new MindStudioError(
1339
+ `Insert into '${this._config.tableName}' succeeded but returned no row. This may indicate a constraint violation.`,
1340
+ "insert_failed",
1341
+ 500
1342
+ );
1280
1343
  });
1281
1344
  const result = isArray ? rows : rows[0];
1282
1345
  this._syncRolesIfNeeded(
@@ -1300,6 +1363,13 @@ var Table = class {
1300
1363
  this._config.columns
1301
1364
  );
1302
1365
  return new Mutation(this._config, [query], (results) => {
1366
+ if (!results[0]?.rows[0]) {
1367
+ throw new MindStudioError(
1368
+ `Row not found: no row with ID '${id}' in table '${this._config.tableName}'`,
1369
+ "row_not_found",
1370
+ 404
1371
+ );
1372
+ }
1303
1373
  const result = deserializeRow(
1304
1374
  results[0].rows[0],
1305
1375
  this._config.columns
@@ -1314,7 +1384,9 @@ var Table = class {
1314
1384
  }
1315
1385
  remove(id) {
1316
1386
  const query = buildDelete(this._config.tableName, `id = ?`, [id]);
1317
- return new Mutation(this._config, [query], () => void 0);
1387
+ return new Mutation(this._config, [query], (results) => ({
1388
+ deleted: results[0].changes > 0
1389
+ }));
1318
1390
  }
1319
1391
  /**
1320
1392
  * Remove all rows matching a predicate. Returns the count removed.
@@ -1348,7 +1420,7 @@ var Table = class {
1348
1420
  }
1349
1421
  clear() {
1350
1422
  const query = buildDelete(this._config.tableName);
1351
- return new Mutation(this._config, [query], () => void 0);
1423
+ return new Mutation(this._config, [query], (results) => results[0].changes);
1352
1424
  }
1353
1425
  /**
1354
1426
  * Insert a row, or update it if a row with the same unique key already
@@ -1366,6 +1438,15 @@ var Table = class {
1366
1438
  this._validateUniqueConstraint(conflictColumns);
1367
1439
  const withDefaults = this._config.defaults ? { ...this._config.defaults, ...data } : data;
1368
1440
  this._checkManagedColumns(withDefaults);
1441
+ for (const col of conflictColumns) {
1442
+ if (!(col in withDefaults)) {
1443
+ throw new MindStudioError(
1444
+ `Upsert on ${this._config.tableName} requires "${col}" in data (conflict key)`,
1445
+ "missing_conflict_key",
1446
+ 400
1447
+ );
1448
+ }
1449
+ }
1369
1450
  const query = buildUpsert(
1370
1451
  this._config.tableName,
1371
1452
  withDefaults,
@@ -1373,6 +1454,13 @@ var Table = class {
1373
1454
  this._config.columns
1374
1455
  );
1375
1456
  return new Mutation(this._config, [query], (results) => {
1457
+ if (!results[0]?.rows[0]) {
1458
+ throw new MindStudioError(
1459
+ `Upsert into ${this._config.tableName} returned no row`,
1460
+ "upsert_failed",
1461
+ 500
1462
+ );
1463
+ }
1376
1464
  const result = deserializeRow(
1377
1465
  results[0].rows[0],
1378
1466
  this._config.columns
@@ -1524,7 +1612,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
1524
1612
  if (c.type === "query") {
1525
1613
  if (!c.query && c.predicates?.length) {
1526
1614
  console.warn(
1527
- `[mindstudio] db.batch(): filter on ${c.config.tableName} could not be compiled to SQL \u2014 processing in JS`
1615
+ `[mindstudio] db.batch(): filter on '${c.config.tableName}' could not be compiled to SQL \u2014 processing in JS`
1528
1616
  );
1529
1617
  }
1530
1618
  return Query._processResults(results[0], c);
@@ -2999,9 +3087,14 @@ var stepMetadata = {
2999
3087
  },
3000
3088
  "sendEmail": {
3001
3089
  stepType: "sendEmail",
3002
- description: "Send an email to one or more configured recipient addresses.",
3003
- usageNotes: '- Recipient email addresses are resolved from OAuth connections configured by the app creator. The user running the workflow does not specify the recipient directly.\n- If the body is a URL to a hosted HTML file on the CDN, the HTML is fetched and used as the email body.\n- When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model.\n- connectionId can be a comma-separated list to send to multiple recipients.\n- The special connectionId "trigger_email" uses the email address that triggered the workflow.',
3004
- inputSchema: { "type": "object", "properties": { "subject": { "type": "string", "description": "Email subject line" }, "body": { "type": "string", "description": "Email body content (plain text, markdown, HTML, or a CDN URL to an HTML file)" }, "connectionId": { "type": "string", "description": "OAuth connection ID(s) for the recipient(s), comma-separated for multiple" }, "generateHtml": { "type": "boolean", "description": "When true, auto-convert the body text into a styled HTML email using AI" }, "generateHtmlInstructions": { "type": "string", "description": "Natural language instructions for the HTML generation style" }, "generateHtmlModelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "properties": {}, "required": [], "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Model settings override for HTML generation" }, "attachments": { "type": "array", "items": { "type": "string" }, "description": "URLs of files to attach to the email" } }, "required": ["subject", "body"] },
3090
+ description: "Send an email to one or more recipient addresses.",
3091
+ usageNotes: `- Use the "to" field to send to specific email addresses directly. For v2 apps, recipients must be verified users in the app's user table.
3092
+ - Alternatively, recipient email addresses can be resolved from OAuth connections configured by the app creator via connectionId. The user running the workflow does not specify the recipient directly.
3093
+ - If the body is a URL to a hosted HTML file on the CDN, the HTML is fetched and used as the email body.
3094
+ - When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model.
3095
+ - connectionId can be a comma-separated list to send to multiple recipients.
3096
+ - The special connectionId "trigger_email" uses the email address that triggered the workflow.`,
3097
+ inputSchema: { "type": "object", "properties": { "subject": { "type": "string", "description": "Email subject line" }, "body": { "type": "string", "description": "Email body content (plain text, markdown, HTML, or a CDN URL to an HTML file)" }, "to": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "connectionId": { "type": "string", "description": "OAuth connection ID(s) for the recipient(s), comma-separated for multiple" }, "generateHtml": { "type": "boolean", "description": "When true, auto-convert the body text into a styled HTML email using AI" }, "generateHtmlInstructions": { "type": "string", "description": "Natural language instructions for the HTML generation style" }, "generateHtmlModelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "properties": {}, "required": [], "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Model settings override for HTML generation" }, "attachments": { "type": "array", "items": { "type": "string" }, "description": "URLs of files to attach to the email" } }, "required": ["subject", "body"] },
3005
3098
  outputSchema: { "type": "object", "properties": { "recipients": { "type": "array", "items": { "type": "string" }, "description": "Email addresses the message was sent to" } }, "required": ["recipients"] }
3006
3099
  },
3007
3100
  "sendGmailDraft": {
@@ -3323,7 +3416,7 @@ var MindStudioAgent = class {
3323
3416
  const res = await fetch(data.outputUrl);
3324
3417
  if (!res.ok) {
3325
3418
  throw new MindStudioError(
3326
- `Failed to fetch output from S3: ${res.status} ${res.statusText}`,
3419
+ `Failed to fetch ${stepType} output from S3: ${res.status} ${res.statusText}`,
3327
3420
  "output_fetch_error",
3328
3421
  res.status
3329
3422
  );
@@ -3386,13 +3479,27 @@ var MindStudioAgent = class {
3386
3479
  this._httpConfig.rateLimiter.updateFromHeaders(res.headers);
3387
3480
  if (!res.ok) {
3388
3481
  this._httpConfig.rateLimiter.release();
3389
- const errorBody = await res.json().catch(() => ({}));
3390
- throw new MindStudioError(
3391
- errorBody.message || `${res.status} ${res.statusText}`,
3392
- errorBody.code || "api_error",
3393
- res.status,
3394
- errorBody
3395
- );
3482
+ let message = `${res.status} ${res.statusText}`;
3483
+ let code = "api_error";
3484
+ let details;
3485
+ try {
3486
+ const text = await res.text();
3487
+ try {
3488
+ const body2 = JSON.parse(text);
3489
+ details = body2;
3490
+ const errMsg = (typeof body2.error === "string" ? body2.error : void 0) ?? (typeof body2.message === "string" ? body2.message : void 0) ?? (typeof body2.details === "string" ? body2.details : void 0);
3491
+ if (errMsg) message = errMsg;
3492
+ else if (body2.error || body2.message || body2.details) {
3493
+ message = JSON.stringify(body2.error ?? body2.message ?? body2.details);
3494
+ }
3495
+ if (body2.code) code = body2.code;
3496
+ } catch {
3497
+ const stripped = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
3498
+ if (stripped) message = stripped.slice(0, 200);
3499
+ }
3500
+ } catch {
3501
+ }
3502
+ throw new MindStudioError(`[${stepType}] ${message}`, code, res.status, details);
3396
3503
  }
3397
3504
  const headers = res.headers;
3398
3505
  try {
@@ -3425,7 +3532,7 @@ var MindStudioAgent = class {
3425
3532
  };
3426
3533
  } else if (event.type === "error") {
3427
3534
  throw new MindStudioError(
3428
- event.error || "Step execution failed",
3535
+ `[${stepType}] ${event.error || "Step execution failed"}`,
3429
3536
  "step_error",
3430
3537
  500
3431
3538
  );
@@ -3464,7 +3571,7 @@ var MindStudioAgent = class {
3464
3571
  }
3465
3572
  if (!doneEvent) {
3466
3573
  throw new MindStudioError(
3467
- "Stream ended without a done event",
3574
+ `[${stepType}] Stream ended unexpectedly without completing. The step execution may have been interrupted.`,
3468
3575
  "stream_error",
3469
3576
  500
3470
3577
  );
@@ -3476,7 +3583,7 @@ var MindStudioAgent = class {
3476
3583
  const s3Res = await fetch(doneEvent.outputUrl);
3477
3584
  if (!s3Res.ok) {
3478
3585
  throw new MindStudioError(
3479
- `Failed to fetch output from S3: ${s3Res.status} ${s3Res.statusText}`,
3586
+ `Failed to fetch ${stepType} output from S3: ${s3Res.status} ${s3Res.statusText}`,
3480
3587
  "output_fetch_error",
3481
3588
  s3Res.status
3482
3589
  );
@@ -3826,12 +3933,18 @@ var MindStudioAgent = class {
3826
3933
  * ```
3827
3934
  */
3828
3935
  get auth() {
3936
+ if (this._authType === "internal") {
3937
+ const ai = globalThis.ai;
3938
+ if (ai?.auth) {
3939
+ return new AuthContext(ai.auth);
3940
+ }
3941
+ }
3829
3942
  if (!this._auth) {
3830
3943
  this._trySandboxHydration();
3831
3944
  }
3832
3945
  if (!this._auth) {
3833
3946
  throw new MindStudioError(
3834
- "Auth context not yet loaded. Call `await agent.ensureContext()` or perform any db operation first (which auto-hydrates context). Inside the MindStudio sandbox, context is loaded automatically.",
3947
+ "Auth context not loaded. Call `await agent.ensureContext()` first, or perform any db operation (which auto-loads context).",
3835
3948
  "context_not_loaded",
3836
3949
  400
3837
3950
  );
@@ -3962,8 +4075,11 @@ var MindStudioAgent = class {
3962
4075
  const text = await res.text();
3963
4076
  try {
3964
4077
  const body = JSON.parse(text);
3965
- const errMsg = body.error ?? body.message ?? body.details;
4078
+ const errMsg = (typeof body.error === "string" ? body.error : void 0) ?? (typeof body.message === "string" ? body.message : void 0) ?? (typeof body.details === "string" ? body.details : void 0);
3966
4079
  if (errMsg) message = errMsg;
4080
+ else if (body.error || body.message || body.details) {
4081
+ message = JSON.stringify(body.error ?? body.message ?? body.details);
4082
+ }
3967
4083
  if (body.code) code = body.code;
3968
4084
  } catch {
3969
4085
  if (text && text.length < 500) message = text;
@@ -4002,13 +4118,12 @@ var MindStudioAgent = class {
4002
4118
  if (!res.ok) {
4003
4119
  const text = await res.text().catch(() => "");
4004
4120
  console.warn(
4005
- `[mindstudio] Failed to sync roles for user ${userId}: ${res.status} ${text}`
4121
+ `[mindstudio] Role sync failed for user ${userId} (${res.status}${text ? ": " + text.slice(0, 100) : ""}). Roles were saved to the database but may not be reflected in auth.hasRole() until the next successful write.`
4006
4122
  );
4007
4123
  }
4008
4124
  } catch (err) {
4009
4125
  console.warn(
4010
- `[mindstudio] Failed to sync roles for user ${userId}:`,
4011
- err
4126
+ `[mindstudio] Role sync failed for user ${userId}: network error. Roles were saved to the database but may not be reflected in auth.hasRole() until the next successful write.`
4012
4127
  );
4013
4128
  }
4014
4129
  }