@mindstudio-ai/agent 0.1.103 → 0.1.105

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
@@ -462,6 +462,59 @@ function buildSelect(table, options = {}) {
462
462
  if (options.offset != null) sql += ` OFFSET ${options.offset}`;
463
463
  return { sql, params: params.length > 0 ? params : void 0 };
464
464
  }
465
+ var SQL_IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
466
+ function assertSqlIdentifier(name, context) {
467
+ if (!SQL_IDENTIFIER_RE.test(name)) {
468
+ throw new MindStudioError(
469
+ `Invalid ${context}: "${name}" \u2014 must be a plain identifier (letters, digits, _ and $, not starting with a digit).`,
470
+ "invalid_identifier",
471
+ 400
472
+ );
473
+ }
474
+ }
475
+ function aggregateExpr(fn, column) {
476
+ switch (fn) {
477
+ case "count":
478
+ return "COUNT(*)";
479
+ case "sum":
480
+ return `TOTAL(${column})`;
481
+ case "avg":
482
+ return `AVG(${column})`;
483
+ case "min":
484
+ return `MIN(${column})`;
485
+ case "max":
486
+ return `MAX(${column})`;
487
+ case "countDistinct":
488
+ return `COUNT(DISTINCT ${column})`;
489
+ }
490
+ }
491
+ function buildScalarAggregate(table, fn, column, where, whereParams) {
492
+ if (column != null) assertSqlIdentifier(column, "aggregate column");
493
+ let sql = `SELECT ${aggregateExpr(fn, column ?? void 0)} AS value FROM ${table}`;
494
+ if (where) sql += ` WHERE ${where}`;
495
+ return { sql, params: whereParams?.length ? whereParams : void 0 };
496
+ }
497
+ function buildGroupedAggregate(table, options) {
498
+ for (const col of options.by) assertSqlIdentifier(col, "group-by column");
499
+ for (const term of options.select) {
500
+ assertSqlIdentifier(term.alias, "aggregate alias");
501
+ if (term.column != null) assertSqlIdentifier(term.column, "aggregate column");
502
+ }
503
+ if (options.orderBy != null) assertSqlIdentifier(options.orderBy, "aggregate order key");
504
+ const selectList = [
505
+ ...options.by,
506
+ ...options.select.map((t) => `${aggregateExpr(t.fn, t.column)} AS ${t.alias}`)
507
+ ].join(", ");
508
+ let sql = `SELECT ${selectList} FROM ${table}`;
509
+ if (options.where) sql += ` WHERE ${options.where}`;
510
+ if (options.by.length > 0) sql += ` GROUP BY ${options.by.join(", ")}`;
511
+ if (options.orderBy) sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
512
+ if (options.limit != null) sql += ` LIMIT ${options.limit}`;
513
+ return {
514
+ sql,
515
+ params: options.whereParams?.length ? options.whereParams : void 0
516
+ };
517
+ }
465
518
  function buildExists(table, where, whereParams, negate) {
466
519
  const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
467
520
  const fn = negate ? "NOT EXISTS" : "EXISTS";
@@ -1119,6 +1172,187 @@ function isComparisonOp(value) {
1119
1172
  return value in JS_TO_SQL_OP;
1120
1173
  }
1121
1174
 
1175
+ // src/db/aggregate.ts
1176
+ var IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
1177
+ var AGGREGATE_FNS = [
1178
+ "count",
1179
+ "sum",
1180
+ "avg",
1181
+ "min",
1182
+ "max",
1183
+ "countDistinct"
1184
+ ];
1185
+ function normalizeAggregateSpec(spec) {
1186
+ const by = [...spec.by ?? []];
1187
+ for (const col of by) {
1188
+ if (typeof col !== "string" || !IDENTIFIER_RE.test(col)) {
1189
+ throw new MindStudioError(
1190
+ `aggregate(): invalid group-by column "${col}" \u2014 column names are plain identifiers.`,
1191
+ "invalid_column",
1192
+ 400
1193
+ );
1194
+ }
1195
+ }
1196
+ const aliases = Object.keys(spec.select ?? {});
1197
+ if (aliases.length === 0) {
1198
+ throw new MindStudioError(
1199
+ "aggregate(): select must contain at least one aggregate, e.g. { n: { count: true } }.",
1200
+ "invalid_aggregate_spec",
1201
+ 400
1202
+ );
1203
+ }
1204
+ const select = aliases.map((alias) => {
1205
+ if (!IDENTIFIER_RE.test(alias)) {
1206
+ throw new MindStudioError(
1207
+ `aggregate(): invalid alias "${alias}" \u2014 result property names are plain identifiers.`,
1208
+ "invalid_aggregate_alias",
1209
+ 400
1210
+ );
1211
+ }
1212
+ if (by.includes(alias)) {
1213
+ throw new MindStudioError(
1214
+ `aggregate(): alias "${alias}" collides with a group-by column of the same name.`,
1215
+ "invalid_aggregate_alias",
1216
+ 400
1217
+ );
1218
+ }
1219
+ const term = spec.select[alias];
1220
+ const fns = AGGREGATE_FNS.filter((fn2) => term && fn2 in term);
1221
+ if (fns.length !== 1) {
1222
+ throw new MindStudioError(
1223
+ `aggregate(): term "${alias}" must have exactly one aggregate key (count, sum, avg, min, max, countDistinct).`,
1224
+ "invalid_aggregate_spec",
1225
+ 400
1226
+ );
1227
+ }
1228
+ const fn = fns[0];
1229
+ if (fn === "count") {
1230
+ if (term[fn] !== true) {
1231
+ throw new MindStudioError(
1232
+ `aggregate(): term "${alias}" \u2014 count takes \`true\`, e.g. { count: true }.`,
1233
+ "invalid_aggregate_spec",
1234
+ 400
1235
+ );
1236
+ }
1237
+ return { alias, fn };
1238
+ }
1239
+ const column = term[fn];
1240
+ if (typeof column !== "string" || !IDENTIFIER_RE.test(column)) {
1241
+ throw new MindStudioError(
1242
+ `aggregate(): term "${alias}" \u2014 invalid column "${String(column)}".`,
1243
+ "invalid_column",
1244
+ 400
1245
+ );
1246
+ }
1247
+ return { alias, fn, column };
1248
+ });
1249
+ if (spec.orderBy != null) {
1250
+ if (!aliases.includes(spec.orderBy) && !by.includes(spec.orderBy)) {
1251
+ throw new MindStudioError(
1252
+ `aggregate(): orderBy "${spec.orderBy}" is neither a select alias nor a group-by column.`,
1253
+ "invalid_aggregate_order",
1254
+ 400
1255
+ );
1256
+ }
1257
+ }
1258
+ return {
1259
+ by,
1260
+ select,
1261
+ orderBy: spec.orderBy,
1262
+ desc: spec.desc,
1263
+ limit: spec.limit
1264
+ };
1265
+ }
1266
+ function aggregateValues(fn, values) {
1267
+ const present = values.filter((v) => v != null);
1268
+ switch (fn) {
1269
+ case "count":
1270
+ return values.length;
1271
+ case "sum":
1272
+ return present.reduce((s, v) => s + v, 0);
1273
+ case "avg":
1274
+ return present.length === 0 ? null : present.reduce((s, v) => s + v, 0) / present.length;
1275
+ case "countDistinct":
1276
+ return new Set(present).size;
1277
+ case "min":
1278
+ return present.length === 0 ? null : present.reduce((m, v) => v < m ? v : m);
1279
+ case "max":
1280
+ return present.length === 0 ? null : present.reduce((m, v) => v > m ? v : m);
1281
+ }
1282
+ }
1283
+ function compareForOrder(a, b) {
1284
+ if (a == null && b == null) return 0;
1285
+ if (a == null) return -1;
1286
+ if (b == null) return 1;
1287
+ if (a < b) return -1;
1288
+ if (a > b) return 1;
1289
+ return 0;
1290
+ }
1291
+ function computeAggregateInJs(rows, spec) {
1292
+ const groups = /* @__PURE__ */ new Map();
1293
+ for (const row of rows) {
1294
+ const key = JSON.stringify(spec.by.map((col) => row[col]));
1295
+ const group = groups.get(key);
1296
+ if (group) group.push(row);
1297
+ else groups.set(key, [row]);
1298
+ }
1299
+ if (spec.by.length === 0 && groups.size === 0) {
1300
+ groups.set("[]", []);
1301
+ }
1302
+ let out = Array.from(groups.values()).map((groupRows) => {
1303
+ const result = {};
1304
+ for (const col of spec.by) {
1305
+ result[col] = groupRows[0]?.[col];
1306
+ }
1307
+ for (const term of spec.select) {
1308
+ result[term.alias] = aggregateValues(
1309
+ term.fn,
1310
+ term.column != null ? groupRows.map((r) => r[term.column]) : groupRows
1311
+ );
1312
+ }
1313
+ return result;
1314
+ });
1315
+ if (spec.orderBy) {
1316
+ const key = spec.orderBy;
1317
+ out.sort((a, b) => compareForOrder(a[key], b[key]));
1318
+ if (spec.desc) out.reverse();
1319
+ }
1320
+ if (spec.limit != null) {
1321
+ out = out.slice(0, spec.limit);
1322
+ }
1323
+ return spec.by.length === 0 ? out[0] : out;
1324
+ }
1325
+ function deserializeAggregateRows(rows, spec, columns) {
1326
+ const schemaFor = /* @__PURE__ */ new Map();
1327
+ for (const col of spec.by) {
1328
+ schemaFor.set(
1329
+ col,
1330
+ columns.filter((c) => c.name === col)
1331
+ );
1332
+ }
1333
+ for (const term of spec.select) {
1334
+ if ((term.fn === "min" || term.fn === "max") && term.column != null) {
1335
+ schemaFor.set(
1336
+ term.alias,
1337
+ columns.filter((c) => c.name === term.column).map((c) => ({ ...c, name: term.alias }))
1338
+ );
1339
+ }
1340
+ }
1341
+ return rows.map((raw) => {
1342
+ const row = raw;
1343
+ const result = {};
1344
+ for (const [key, value] of Object.entries(row)) {
1345
+ const schema = schemaFor.get(key);
1346
+ if (schema) {
1347
+ result[key] = deserializeRow({ [key]: value }, schema)[key];
1348
+ } else {
1349
+ result[key] = value;
1350
+ }
1351
+ }
1352
+ return result;
1353
+ });
1354
+ }
1355
+
1122
1356
  // src/db/query.ts
1123
1357
  var Query = class _Query {
1124
1358
  _predicates;
@@ -1130,6 +1364,8 @@ var Query = class _Query {
1130
1364
  /** @internal Pre-compiled WHERE clause (bypasses predicate compiler). Used by Table.get(). */
1131
1365
  _rawWhere;
1132
1366
  _rawWhereParams;
1367
+ /** @internal See AggregateState — set by aggregate terminals. */
1368
+ _aggregate;
1133
1369
  /** @internal Post-process transform applied after row deserialization. */
1134
1370
  _postProcess;
1135
1371
  constructor(config, options) {
@@ -1142,6 +1378,7 @@ var Query = class _Query {
1142
1378
  this._postProcess = options?.postProcess;
1143
1379
  this._rawWhere = options?.rawWhere;
1144
1380
  this._rawWhereParams = options?.rawWhereParams;
1381
+ this._aggregate = options?.aggregate;
1145
1382
  }
1146
1383
  _clone(overrides) {
1147
1384
  return new _Query(this._config, {
@@ -1152,7 +1389,8 @@ var Query = class _Query {
1152
1389
  offset: overrides.offset ?? this._offset,
1153
1390
  postProcess: overrides.postProcess,
1154
1391
  rawWhere: this._rawWhere,
1155
- rawWhereParams: this._rawWhereParams
1392
+ rawWhereParams: this._rawWhereParams,
1393
+ aggregate: overrides.aggregate ?? this._aggregate
1156
1394
  });
1157
1395
  }
1158
1396
  filter(predicate, bindings) {
@@ -1190,9 +1428,60 @@ var Query = class _Query {
1190
1428
  }
1191
1429
  count() {
1192
1430
  return this._clone({
1431
+ aggregate: { kind: "scalar", fn: "count" },
1193
1432
  postProcess: (rows) => rows.length
1194
1433
  });
1195
1434
  }
1435
+ /**
1436
+ * Sum of a numeric field across matching rows. Compiles to a SQL
1437
+ * aggregate (TOTAL) when possible — no rows are fetched. Returns 0 for
1438
+ * an empty set; NULL values are skipped (SQL semantics).
1439
+ */
1440
+ sum(accessor) {
1441
+ return this._clone({
1442
+ aggregate: { kind: "scalar", fn: "sum", accessor },
1443
+ postProcess: (rows) => rows.reduce((total, row) => {
1444
+ const value = accessor(row);
1445
+ return value == null ? total : total + value;
1446
+ }, 0)
1447
+ });
1448
+ }
1449
+ /**
1450
+ * Average of a numeric field across matching rows. Compiles to SQL AVG
1451
+ * when possible. Returns null for an empty set (or when every value is
1452
+ * null) — SQL AVG semantics.
1453
+ */
1454
+ avg(accessor) {
1455
+ return this._clone({
1456
+ aggregate: { kind: "scalar", fn: "avg", accessor },
1457
+ postProcess: (rows) => {
1458
+ const values = rows.map((row) => accessor(row)).filter((v) => v != null);
1459
+ return values.length === 0 ? null : values.reduce((s, v) => s + v, 0) / values.length;
1460
+ }
1461
+ });
1462
+ }
1463
+ /**
1464
+ * Number of distinct non-null values of a field across matching rows.
1465
+ * Compiles to SQL COUNT(DISTINCT col) when possible.
1466
+ */
1467
+ countDistinct(accessor) {
1468
+ return this._clone({
1469
+ aggregate: { kind: "scalar", fn: "countDistinct", accessor },
1470
+ postProcess: (rows) => new Set(rows.map((row) => accessor(row)).filter((v) => v != null)).size
1471
+ });
1472
+ }
1473
+ aggregate(spec) {
1474
+ const normalized = normalizeAggregateSpec(
1475
+ spec
1476
+ );
1477
+ return this._clone({
1478
+ aggregate: { kind: "grouped", spec: normalized },
1479
+ postProcess: (rows) => computeAggregateInJs(
1480
+ rows,
1481
+ normalized
1482
+ )
1483
+ });
1484
+ }
1196
1485
  some() {
1197
1486
  return this._clone({
1198
1487
  limit: 1,
@@ -1263,6 +1552,10 @@ var Query = class _Query {
1263
1552
  const compiled = this._compilePredicates();
1264
1553
  const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
1265
1554
  if (compiled.allSql) {
1555
+ const agg = this._compileAggregateSql(compiled.sqlWhere);
1556
+ if (agg) {
1557
+ return { type: "query", query: agg.query, fallbackQuery: null, config: this._config, processRaw: agg.processRaw };
1558
+ }
1266
1559
  const query = buildSelect(this._config.tableName, {
1267
1560
  where: compiled.sqlWhere || void 0,
1268
1561
  orderBy: sortField ?? void 0,
@@ -1294,6 +1587,9 @@ var Query = class _Query {
1294
1587
  * For JS-fallback queries: filter, sort, and slice in JS.
1295
1588
  */
1296
1589
  static _processResults(result, compiled) {
1590
+ if (compiled.processRaw) {
1591
+ return compiled.processRaw(result);
1592
+ }
1297
1593
  const rows = result.rows.map(
1298
1594
  (row) => deserializeRow(
1299
1595
  row,
@@ -1326,10 +1622,81 @@ var Query = class _Query {
1326
1622
  // PromiseLike
1327
1623
  // -------------------------------------------------------------------------
1328
1624
  then(onfulfilled, onrejected) {
1329
- const promise = this._execute().then(
1330
- (rows) => this._postProcess ? this._postProcess(rows) : rows
1331
- );
1332
- return promise.then(onfulfilled, onrejected);
1625
+ return this._run().then(onfulfilled, onrejected);
1626
+ }
1627
+ async _run() {
1628
+ if (this._aggregate && !this._rawWhere) {
1629
+ const compiled = this._compilePredicates();
1630
+ if (compiled.allSql) {
1631
+ const agg = this._compileAggregateSql(compiled.sqlWhere);
1632
+ if (agg) {
1633
+ const results = await this._config.executeBatch([agg.query]);
1634
+ return agg.processRaw(results[0]);
1635
+ }
1636
+ }
1637
+ }
1638
+ const rows = await this._execute();
1639
+ return this._postProcess ? this._postProcess(rows) : rows;
1640
+ }
1641
+ /**
1642
+ * @internal Compile the pending aggregate into a single SQL statement +
1643
+ * result processor. Returns null (→ fetch path, postProcess computes the
1644
+ * aggregate) when there is no aggregate, when a pre-compiled WHERE is in
1645
+ * play (Table.get()), when limit/offset are set (aggregate-the-page
1646
+ * semantics — COUNT(*)/TOTAL can't express "over the limited page"), or
1647
+ * when a scalar accessor doesn't extract to a column (computed accessors
1648
+ * stay correct via JS, with a warning to prompt optimization).
1649
+ */
1650
+ _compileAggregateSql(sqlWhere) {
1651
+ const agg = this._aggregate;
1652
+ if (!agg || this._rawWhere || this._limit != null || this._offset != null) {
1653
+ return null;
1654
+ }
1655
+ const where = sqlWhere || void 0;
1656
+ if (agg.kind === "scalar") {
1657
+ let column = null;
1658
+ if (agg.accessor) {
1659
+ column = extractFieldName(agg.accessor);
1660
+ if (column == null) {
1661
+ console.warn(
1662
+ `[mindstudio] ${agg.fn}() accessor on '${this._config.tableName}' could not be compiled to SQL \u2014 fetching matching rows and computing in JS`
1663
+ );
1664
+ return null;
1665
+ }
1666
+ }
1667
+ const query2 = buildScalarAggregate(
1668
+ this._config.tableName,
1669
+ agg.fn,
1670
+ column,
1671
+ where
1672
+ );
1673
+ const processRaw2 = (result) => {
1674
+ const value = result.rows[0]?.value;
1675
+ if (agg.fn === "avg") {
1676
+ return value == null ? null : Number(value);
1677
+ }
1678
+ return Number(value ?? 0);
1679
+ };
1680
+ return { query: query2, processRaw: processRaw2 };
1681
+ }
1682
+ const spec = agg.spec;
1683
+ const query = buildGroupedAggregate(this._config.tableName, {
1684
+ by: spec.by,
1685
+ select: spec.select,
1686
+ where,
1687
+ orderBy: spec.orderBy,
1688
+ desc: spec.desc,
1689
+ limit: spec.limit
1690
+ });
1691
+ const processRaw = (result) => {
1692
+ const rows = deserializeAggregateRows(
1693
+ result.rows,
1694
+ spec,
1695
+ this._config.columns
1696
+ );
1697
+ return spec.by.length === 0 ? rows[0] : rows;
1698
+ };
1699
+ return { query, processRaw };
1333
1700
  }
1334
1701
  catch(onrejected) {
1335
1702
  return this.then(void 0, onrejected);
@@ -1560,7 +1927,34 @@ var Table = class {
1560
1927
  max(accessor) {
1561
1928
  return this.sortBy(accessor).reverse().first();
1562
1929
  }
1563
- /** Group rows by a field. Returns a Map. */
1930
+ /**
1931
+ * Sum of a numeric field across all rows (0 for an empty table).
1932
+ * Runs as a SQL aggregate — no rows are fetched. Chain from filter()
1933
+ * to sum a subset: `Orders.filter(...).sum(o => o.amount)`.
1934
+ */
1935
+ sum(accessor) {
1936
+ return this.toArray().sum(accessor);
1937
+ }
1938
+ /**
1939
+ * Average of a numeric field across all rows (null for an empty table).
1940
+ * Runs as a SQL aggregate — no rows are fetched.
1941
+ */
1942
+ avg(accessor) {
1943
+ return this.toArray().avg(accessor);
1944
+ }
1945
+ /**
1946
+ * Number of distinct non-null values of a field across all rows.
1947
+ * Runs as a SQL aggregate — no rows are fetched.
1948
+ */
1949
+ countDistinct(accessor) {
1950
+ return this.toArray().countDistinct(accessor);
1951
+ }
1952
+ aggregate(spec) {
1953
+ return this.toArray().aggregate(spec);
1954
+ }
1955
+ /** Group rows by a field. Returns a Map of FULL rows per group — it
1956
+ * fetches everything. For grouped counts/sums/averages over tables that
1957
+ * can grow large, use `aggregate()` instead (SQL GROUP BY, no row fetch). */
1564
1958
  groupBy(accessor) {
1565
1959
  return new Query(this._config).groupBy(accessor);
1566
1960
  }
@@ -1796,6 +2190,57 @@ var Table = class {
1796
2190
  }
1797
2191
  };
1798
2192
 
2193
+ // src/db/raw.ts
2194
+ var RawQuery = class _RawQuery {
2195
+ _databaseId;
2196
+ _executeBatch;
2197
+ _query;
2198
+ constructor(databaseId, executeBatch, query) {
2199
+ this._databaseId = databaseId;
2200
+ this._executeBatch = executeBatch;
2201
+ this._query = query;
2202
+ }
2203
+ // -------------------------------------------------------------------------
2204
+ // PromiseLike — executes on await
2205
+ // -------------------------------------------------------------------------
2206
+ then(onfulfilled, onrejected) {
2207
+ return this._execute().then(onfulfilled, onrejected);
2208
+ }
2209
+ catch(onrejected) {
2210
+ return this._execute().catch(onrejected);
2211
+ }
2212
+ // -------------------------------------------------------------------------
2213
+ // Batch support
2214
+ // -------------------------------------------------------------------------
2215
+ /** @internal Compile for db.batch(). */
2216
+ _compile() {
2217
+ return {
2218
+ type: "raw",
2219
+ query: this._query,
2220
+ config: { databaseId: this._databaseId },
2221
+ processResult: (result) => _RawQuery._process(result)
2222
+ };
2223
+ }
2224
+ /** @internal Process a raw SQL result. Used by db.batch(). */
2225
+ static _processResults(result, compiled) {
2226
+ return compiled.processResult(result);
2227
+ }
2228
+ // -------------------------------------------------------------------------
2229
+ // Execution
2230
+ // -------------------------------------------------------------------------
2231
+ /** Schema-less deserialization: strips @@user@@ prefixes and best-effort
2232
+ * parses JSON-looking strings; makes no other representation promises. */
2233
+ static _process(result) {
2234
+ return result.rows.map(
2235
+ (row) => deserializeRow(row, [])
2236
+ );
2237
+ }
2238
+ async _execute() {
2239
+ const results = await this._executeBatch([this._query]);
2240
+ return _RawQuery._process(results[0]);
2241
+ }
2242
+ };
2243
+
1799
2244
  // src/db/index.ts
1800
2245
  function createDb(databases, executeBatch, authConfig, syncRoles) {
1801
2246
  return {
@@ -1823,6 +2268,16 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
1823
2268
  fromNow: (ms) => Date.now() + ms,
1824
2269
  // --- User references ---
1825
2270
  userRef: (id) => id.startsWith(USER_PREFIX) ? id.slice(USER_PREFIX.length) : id,
2271
+ // --- Raw SQL (read-only) ---
2272
+ sql(query, params, options) {
2273
+ validateRawSql(query);
2274
+ const database = resolveDatabase(databases, options?.database);
2275
+ return new RawQuery(
2276
+ database.id,
2277
+ (queries) => executeBatch(database.id, queries),
2278
+ { sql: query, params: params?.map(serializeParam) }
2279
+ );
2280
+ },
1826
2281
  // --- Batch execution ---
1827
2282
  batch: ((...operations) => {
1828
2283
  return (async () => {
@@ -1833,8 +2288,11 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
1833
2288
  if (op instanceof Mutation) {
1834
2289
  return op._compile();
1835
2290
  }
2291
+ if (op instanceof RawQuery) {
2292
+ return op._compile();
2293
+ }
1836
2294
  throw new MindStudioError(
1837
- "db.batch() only accepts Query and Mutation objects (from .filter(), .update(), .push(), etc.)",
2295
+ "db.batch() only accepts Query, Mutation, and RawQuery objects (from .filter(), .update(), .push(), db.sql(), etc.)",
1838
2296
  "invalid_batch_operation",
1839
2297
  400
1840
2298
  );
@@ -1847,6 +2305,8 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
1847
2305
  if (c.type === "query") {
1848
2306
  const sqlQuery = c.query ?? c.fallbackQuery;
1849
2307
  groups.get(dbId).push({ opIndex: i, sqlQueries: [sqlQuery] });
2308
+ } else if (c.type === "raw") {
2309
+ groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
1850
2310
  } else {
1851
2311
  groups.get(dbId).push({ opIndex: i, sqlQueries: c.queries });
1852
2312
  }
@@ -1879,6 +2339,8 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
1879
2339
  );
1880
2340
  }
1881
2341
  return Query._processResults(results[0], c);
2342
+ } else if (c.type === "raw") {
2343
+ return RawQuery._processResults(results[0], c);
1882
2344
  } else {
1883
2345
  return Mutation._processResults(results, c);
1884
2346
  }
@@ -1887,7 +2349,23 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
1887
2349
  })
1888
2350
  };
1889
2351
  }
1890
- function resolveTable(databases, tableName, databaseHint) {
2352
+ function validateRawSql(query) {
2353
+ if (typeof query !== "string" || !query.trim()) {
2354
+ throw new MindStudioError(
2355
+ "db.sql() requires a non-empty SQL string.",
2356
+ "invalid_sql",
2357
+ 400
2358
+ );
2359
+ }
2360
+ if (!/^\s*(select|with)\b/i.test(query)) {
2361
+ throw new MindStudioError(
2362
+ "db.sql() is read-only \u2014 the statement must start with SELECT or WITH. Use Table methods (push/update/upsert/remove) for writes.",
2363
+ "sql_read_only",
2364
+ 400
2365
+ );
2366
+ }
2367
+ }
2368
+ function resolveDatabase(databases, hint) {
1891
2369
  if (databases.length === 0) {
1892
2370
  throw new MindStudioError(
1893
2371
  `No databases found in app context. Make sure the app has at least one database configured.`,
@@ -1895,18 +2373,40 @@ function resolveTable(databases, tableName, databaseHint) {
1895
2373
  400
1896
2374
  );
1897
2375
  }
1898
- if (databaseHint) {
2376
+ if (hint) {
1899
2377
  const targetDb = databases.find(
1900
- (db2) => db2.id === databaseHint || db2.name === databaseHint
2378
+ (db2) => db2.id === hint || db2.name === hint
1901
2379
  );
1902
2380
  if (!targetDb) {
1903
2381
  const available = databases.map((db2) => db2.name || db2.id).join(", ");
1904
2382
  throw new MindStudioError(
1905
- `Database "${databaseHint}" not found. Available databases: ${available}`,
2383
+ `Database "${hint}" not found. Available databases: ${available}`,
1906
2384
  "database_not_found",
1907
2385
  400
1908
2386
  );
1909
2387
  }
2388
+ return targetDb;
2389
+ }
2390
+ if (databases.length > 1) {
2391
+ const available = databases.map((db2) => db2.name || db2.id).join(", ");
2392
+ throw new MindStudioError(
2393
+ `This app has multiple databases \u2014 pass { database } to pick one. Available databases: ${available}`,
2394
+ "ambiguous_database",
2395
+ 400
2396
+ );
2397
+ }
2398
+ return databases[0];
2399
+ }
2400
+ function resolveTable(databases, tableName, databaseHint) {
2401
+ if (databases.length === 0) {
2402
+ throw new MindStudioError(
2403
+ `No databases found in app context. Make sure the app has at least one database configured.`,
2404
+ "no_databases",
2405
+ 400
2406
+ );
2407
+ }
2408
+ if (databaseHint) {
2409
+ const targetDb = resolveDatabase(databases, databaseHint);
1910
2410
  const table = targetDb.tables.find((t) => t.name === tableName);
1911
2411
  if (!table) {
1912
2412
  const available = targetDb.tables.map((t) => t.name).join(", ");
@@ -2278,6 +2778,38 @@ function createVoice(call) {
2278
2778
  };
2279
2779
  }
2280
2780
 
2781
+ // src/analytics/index.ts
2782
+ function createAnalytics(call) {
2783
+ return {
2784
+ query(spec) {
2785
+ return call("query", spec);
2786
+ },
2787
+ live() {
2788
+ return call("live", {});
2789
+ },
2790
+ sources(options) {
2791
+ return call("sources", options ?? {});
2792
+ },
2793
+ map(options) {
2794
+ return call("map", options ?? {});
2795
+ },
2796
+ aiSources(options) {
2797
+ return call("ai-sources", options ?? {});
2798
+ },
2799
+ crawlers: {
2800
+ overview(options) {
2801
+ return call("crawlers-overview", options ?? {});
2802
+ },
2803
+ timeseries(options) {
2804
+ return call("crawlers-timeseries", options ?? {});
2805
+ },
2806
+ recent(options) {
2807
+ return call("crawlers-recent", options ?? {});
2808
+ }
2809
+ }
2810
+ };
2811
+ }
2812
+
2281
2813
  // src/task/index.ts
2282
2814
  import { randomUUID as randomUUID2 } from "crypto";
2283
2815
 
@@ -3056,13 +3588,6 @@ var stepMetadata = {
3056
3588
  inputSchema: { "type": "object", "properties": { "webhookUrl": { "type": "string", "description": "Zapier webhook URL to send data to" }, "input": { "type": "object", "properties": {}, "required": [], "description": "Key-value pairs to send as the JSON POST body" } }, "required": ["webhookUrl", "input"] },
3057
3589
  outputSchema: { "type": "object", "properties": { "data": { "description": "Parsed webhook response from Zapier (JSON object, array, or string)" } }, "required": ["data"] }
3058
3590
  },
3059
- "queryAppDatabase": {
3060
- stepType: "queryAppDatabase",
3061
- description: "Execute a SQL query against the app managed database.",
3062
- usageNotes: '- Executes raw SQL against a SQLite database managed by the app.\n- For SELECT queries, returns rows as JSON.\n- For INSERT/UPDATE/DELETE, returns the number of affected rows.\n- Use {{variables}} directly in your SQL. By default they are automatically extracted\n and passed as safe parameterized values (preventing SQL injection).\n Example: INSERT INTO contacts (name, comment) VALUES ({{name}}, {{comment}})\n- Full MindStudio handlebars syntax is supported, including helpers like {{json myVar}},\n {{get myVar "$.path"}}, {{global.orgName}}, etc.\n- Set parameterize to false for raw/dynamic SQL where variables are interpolated directly\n into the query string. Use this when another step generates full or partial SQL, e.g.\n a bulk INSERT with a precomputed VALUES list. The user is responsible for sanitization\n when parameterize is false.',
3063
- inputSchema: { "type": "object", "properties": { "databaseId": { "type": "string", "description": "Name or ID of the app data database to query" }, "sql": { "type": "string", "description": "SQL query to execute. Use {{variables}} directly in the SQL \u2014 they are handled according to the `parameterize` setting.\n\nWhen parameterize is true (default): {{variables}} are extracted from the SQL, replaced with ? placeholders, resolved via the full MindStudio handlebars pipeline, and passed as safe parameterized values to SQLite. This prevents SQL injection. Example: INSERT INTO contacts (name, email) VALUES ({{name}}, {{email}})\n\nWhen parameterize is false: The entire SQL string is resolved via compileString (standard handlebars interpolation) and executed as-is. Use this for dynamic/generated SQL where another step builds the query. The user is responsible for safety. Example: {{generatedInsertQuery}}\n\nAsk the user for the database schema if they have not already provided it." }, "parameterize": { "type": "boolean", "description": "Whether to treat {{variables}} as parameterized query values (default: true).\n\n- true: {{vars}} are extracted, replaced with ?, and passed as bind params. Safe from SQL injection. Use for standard CRUD operations.\n- false: {{vars}} are interpolated directly into the SQL string via handlebars. Use when another step generates full or partial SQL (e.g. bulk inserts with precomputed VALUES). The user is responsible for sanitization." } }, "required": ["databaseId", "sql"] },
3064
- outputSchema: { "type": "object", "properties": { "rows": { "type": "array", "items": {}, "description": "Result rows for SELECT queries (empty array for write queries)" }, "changes": { "type": "number", "description": "Number of rows affected by INSERT, UPDATE, or DELETE queries (0 for SELECT)" } }, "required": ["rows", "changes"] }
3065
- },
3066
3591
  "queryDataSource": {
3067
3592
  stepType: "queryDataSource",
3068
3593
  description: "Legacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Search a vector data source (RAG) and return relevant document chunks.",
@@ -3266,9 +3791,11 @@ var stepMetadata = {
3266
3791
  - Set replyTo to control the Reply-To address for replies.
3267
3792
  - For threaded replies in a shared inbox, set inReplyTo (the Message-ID being replied to) and references (prior Message-IDs in the thread).
3268
3793
  - connectionId can be a comma-separated list to send to multiple recipients.
3269
- - The special connectionId "trigger_email" uses the email address that triggered the workflow.`,
3270
- 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" } }] }, "cc": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "bcc": { "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": { "anyOf": [{ "type": "string" }, { "type": "object", "properties": { "url": { "type": "string" }, "filename": { "type": "string" }, "contentType": { "type": "string" } }, "required": ["url"] }] }, "description": "Files to attach: each entry is a URL string, or an object `{ url, filename?, contentType? }` to control the displayed filename and MIME type." }, "from": { "type": "string", "description": "Custom sender handle \u2014 only for apps with a custom domain or subdomain. Bare handle (`support`), full `support@your-domain.com`, or `Name <support@your-domain.com>`. Must resolve to a domain the app owns." }, "bodyType": { "enum": ["auto", "html", "markdown", "text"], "type": "string", "description": "How to interpret `body`: `auto` (default \u2014 detect HTML, else render markdown), `html` (send as-is), `markdown` (render to HTML), or `text` (send as plain text only, no HTML part). Every send includes a text/plain alternative." }, "text": { "type": "string", "description": "Explicit text/plain alternative body. Auto-derived from `body` if omitted." }, "replyTo": { "type": "string", "description": "Reply-To address for the email." }, "inReplyTo": { "type": "string", "description": "Message-ID this email replies to, for inbox threading (In-Reply-To header)." }, "references": { "type": "array", "items": { "type": "string" }, "description": "Prior Message-IDs in the thread, for inbox threading (References header)." } }, "required": ["subject", "body"] },
3271
- outputSchema: { "type": "object", "properties": { "recipients": { "type": "array", "items": { "type": "string" }, "description": "To addresses the message was sent to." }, "cc": { "type": "array", "items": { "type": "string" }, "description": "Cc addresses on the message (empty if none)." }, "bcc": { "type": "array", "items": { "type": "string" }, "description": "Bcc addresses on the message (empty if none)." }, "from": { "type": "string", "description": "The resolved sender address the message went out as (auto-selected when no `from` is given)." } }, "required": ["recipients", "cc", "bcc", "from"] }
3794
+ - The special connectionId "trigger_email" uses the email address that triggered the workflow.
3795
+ - Sends default to category "marketing": every message carries a one-click unsubscribe header pointing at the app's own domain, and recipients who previously unsubscribed from this app are skipped and returned in "suppressed". Because the unsubscribe link is per-recipient, a marketing send delivers one separate message per recipient and cc/bcc are folded into that list.
3796
+ - Set category to "transactional" for receipts, alerts, password resets and anything a recipient must not be able to opt out of. Transactional sends carry no unsubscribe header, ignore the unsubscribe list, and keep cc/bcc on a single message \u2014 use it whenever cc/bcc semantics matter.`,
3797
+ 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" } }] }, "cc": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "bcc": { "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": { "anyOf": [{ "type": "string" }, { "type": "object", "properties": { "url": { "type": "string" }, "filename": { "type": "string" }, "contentType": { "type": "string" } }, "required": ["url"] }] }, "description": "Files to attach: each entry is a URL string, or an object `{ url, filename?, contentType? }` to control the displayed filename and MIME type." }, "from": { "type": "string", "description": "Custom sender handle \u2014 only for apps with a custom domain or subdomain. Bare handle (`support`), full `support@your-domain.com`, or `Name <support@your-domain.com>`. Must resolve to a domain the app owns." }, "bodyType": { "enum": ["auto", "html", "markdown", "text"], "type": "string", "description": "How to interpret `body`: `auto` (default \u2014 detect HTML, else render markdown), `html` (send as-is), `markdown` (render to HTML), or `text` (send as plain text only, no HTML part). Every send includes a text/plain alternative." }, "text": { "type": "string", "description": "Explicit text/plain alternative body. Auto-derived from `body` if omitted." }, "replyTo": { "type": "string", "description": "Reply-To address for the email." }, "inReplyTo": { "type": "string", "description": "Message-ID this email replies to, for inbox threading (In-Reply-To header)." }, "references": { "type": "array", "items": { "type": "string" }, "description": "Prior Message-IDs in the thread, for inbox threading (References header)." }, "category": { "enum": ["transactional", "marketing"], "type": "string", "description": "`marketing` (default) adds one-click unsubscribe headers and skips recipients who unsubscribed from this app. Use `transactional` for receipts, alerts and password resets: no unsubscribe header, never suppressed, and cc/bcc preserved on a single message." } }, "required": ["subject", "body"] },
3798
+ outputSchema: { "type": "object", "properties": { "recipients": { "type": "array", "items": { "type": "string" }, "description": "To addresses the message was sent to." }, "cc": { "type": "array", "items": { "type": "string" }, "description": "Cc addresses on the message (empty if none)." }, "bcc": { "type": "array", "items": { "type": "string" }, "description": "Bcc addresses on the message (empty if none)." }, "from": { "type": "string", "description": "The resolved sender address the message went out as (auto-selected when no `from` is given)." }, "suppressed": { "type": "array", "items": { "type": "string" }, "description": "Recipients skipped because they unsubscribed from this app. Always empty for a `transactional` send." } }, "required": ["recipients", "cc", "bcc", "from", "suppressed"] }
3272
3799
  },
3273
3800
  "sendGmailDraft": {
3274
3801
  stepType: "sendGmailDraft",
@@ -4733,9 +5260,6 @@ function applyStepMethods(AgentClass) {
4733
5260
  proto.postToZapier = function(step, options) {
4734
5261
  return this.executeStep("postToZapier", step, options);
4735
5262
  };
4736
- proto.queryAppDatabase = function(step, options) {
4737
- return this.executeStep("queryAppDatabase", step, options);
4738
- };
4739
5263
  proto.queryDataSource = function(step, options) {
4740
5264
  return this.executeStep("queryDataSource", step, options);
4741
5265
  };
@@ -4962,6 +5486,7 @@ var MindStudioAgent = class {
4962
5486
  _files;
4963
5487
  _dataSources;
4964
5488
  _voice;
5489
+ _analytics;
4965
5490
  /** @internal Auth type — 'internal' for CALLBACK_TOKEN (managed mode), 'apiKey' otherwise. */
4966
5491
  _authType;
4967
5492
  /** @internal Usage source sent on step executions (from MINDSTUDIO_REQUEST_SOURCE).
@@ -5978,6 +6503,27 @@ var MindStudioAgent = class {
5978
6503
  get files() {
5979
6504
  return this._files ??= createFiles(this._filesRequest.bind(this));
5980
6505
  }
6506
+ /**
6507
+ * The `analytics` namespace — read the app's own traffic + event analytics
6508
+ * (auto-tracked pageviews, `analytics.track()` events). One general
6509
+ * `query()` (metrics × dimensions × filters × time) plus `live()`,
6510
+ * `sources()`, `map()`, `aiSources()`, `crawlers.*`. See {@link Analytics}
6511
+ * for the lifetime-rollup vs 90-day-events split.
6512
+ *
6513
+ * @example
6514
+ * ```ts
6515
+ * const top = await agent.analytics.query({
6516
+ * metrics: ['pageviews', 'visitors'],
6517
+ * dimensions: ['path'],
6518
+ * dateRange: 'all',
6519
+ * });
6520
+ * ```
6521
+ */
6522
+ get analytics() {
6523
+ return this._analytics ??= createAnalytics(
6524
+ this._analyticsRequest.bind(this)
6525
+ );
6526
+ }
5981
6527
  /**
5982
6528
  * Jewel surfaces: arrival-shaped triggers (`propose`) and the app-native
5983
6529
  * approval queue (`queue.list` / `queue.resolve`). See {@link JewelsApi}.
@@ -6106,6 +6652,16 @@ var MindStudioAgent = class {
6106
6652
  fallbackCode: "voice_error"
6107
6653
  });
6108
6654
  }
6655
+ /**
6656
+ * @internal Transport for the `analytics` namespace —
6657
+ * POST /_internal/v2/analytics/<op> with the raw hook token.
6658
+ */
6659
+ async _analyticsRequest(op, body) {
6660
+ return this._brokeredRequest("analytics", op, body, {
6661
+ fallbackMessage: "Analytics read failed",
6662
+ fallbackCode: "analytics_error"
6663
+ });
6664
+ }
6109
6665
  /**
6110
6666
  * @internal Shared shape for the brokered `/_internal/v2/<ns>/<op>` data
6111
6667
  * planes. Factored out rather than copied per namespace so error handling
@@ -6299,6 +6855,24 @@ var MindStudioAgent = class {
6299
6855
  ago: (ms) => Date.now() - ms,
6300
6856
  fromNow: (ms) => Date.now() + ms,
6301
6857
  userRef: (id) => id.startsWith("@@user@@") ? id.slice("@@user@@".length) : id,
6858
+ // Raw SQL — validate synchronously; database resolution needs context,
6859
+ // so it happens inside the executeBatch closure at execution time
6860
+ // (the same deferral lazy defineTable uses).
6861
+ sql: ((query, params, options) => {
6862
+ validateRawSql(query);
6863
+ return new RawQuery(
6864
+ "",
6865
+ async (queries) => {
6866
+ await agent.ensureContext();
6867
+ const database = resolveDatabase(
6868
+ agent._getContext().databases,
6869
+ options?.database
6870
+ );
6871
+ return agent._executeDbBatch(database.id, queries);
6872
+ },
6873
+ { sql: query, params: params?.map(serializeParam) }
6874
+ );
6875
+ }),
6302
6876
  // Batch needs context — hydrate first, then delegate to real db
6303
6877
  batch: ((...queries) => {
6304
6878
  return (async () => {
@@ -6854,7 +7428,6 @@ var monacoSnippets = {
6854
7428
  "postToSlackChannel": { fields: [["channelId", "string"], ["messageType", ["string", "blocks"]], ["message", "string"]], outputKeys: [] },
6855
7429
  "postToX": { fields: [["text", "string"]], outputKeys: [] },
6856
7430
  "postToZapier": { fields: [["webhookUrl", "string"], ["input", "object"]], outputKeys: ["data"] },
6857
- "queryAppDatabase": { fields: [["databaseId", "string"], ["sql", "string"]], outputKeys: ["rows", "changes"] },
6858
7431
  "queryDataSource": { fields: [["dataSourceId", "string"], ["query", "string"], ["maxResults", "number"]], outputKeys: ["text", "chunks", "query", "citations", "latencyMs"] },
6859
7432
  "queryExternalDatabase": { fields: [["query", "string"], ["outputFormat", ["json", "csv"]]], outputKeys: ["data"] },
6860
7433
  "redactPII": { fields: [["input", "string"], ["language", "string"], ["entities", "array"]], outputKeys: ["text"] },
@@ -6880,7 +7453,7 @@ var monacoSnippets = {
6880
7453
  "searchXPosts": { fields: [["query", "string"], ["scope", ["recent", "all"]], ["options", "object"]], outputKeys: ["posts"] },
6881
7454
  "searchYoutube": { fields: [["query", "string"], ["limitPages", "string"], ["filter", "string"], ["filterType", "string"]], outputKeys: ["results"] },
6882
7455
  "searchYoutubeTrends": { fields: [["bp", ["now", "music", "gaming", "films"]], ["hl", "string"], ["gl", "string"]], outputKeys: [] },
6883
- "sendEmail": { fields: [["subject", "string"], ["body", "string"]], outputKeys: ["recipients", "cc", "bcc", "from"] },
7456
+ "sendEmail": { fields: [["subject", "string"], ["body", "string"]], outputKeys: ["recipients", "cc", "bcc", "from", "suppressed"] },
6884
7457
  "sendGmailDraft": { fields: [["draftId", "string"]], outputKeys: [] },
6885
7458
  "sendGmailMessage": { fields: [["to", "string"], ["subject", "string"], ["message", "string"], ["messageType", ["plain", "html", "markdown"]]], outputKeys: ["messageId"] },
6886
7459
  "sendSlackDirectMessage": { fields: [["slackUserId", "string"], ["messageType", ["string", "blocks"]], ["message", "string"]], outputKeys: [] },
@@ -6969,6 +7542,13 @@ var voice = new Proxy({}, {
6969
7542
  return typeof value === "function" ? value.bind(target) : value;
6970
7543
  }
6971
7544
  });
7545
+ var analytics = new Proxy({}, {
7546
+ get(_, prop) {
7547
+ const target = mindstudio.analytics;
7548
+ const value = Reflect.get(target, prop, target);
7549
+ return typeof value === "function" ? value.bind(target) : value;
7550
+ }
7551
+ });
6972
7552
  var session = new Proxy(
6973
7553
  {},
6974
7554
  {
@@ -6990,6 +7570,7 @@ export {
6990
7570
  MindStudioAgent2 as MindStudioAgent,
6991
7571
  MindStudioError,
6992
7572
  Roles,
7573
+ analytics,
6993
7574
  auth,
6994
7575
  blockTypeAliases,
6995
7576
  dataSources,