@mindstudio-ai/agent 0.1.162 → 0.1.164

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.
@@ -1433,7 +1433,7 @@ function getOrOpen(url, token) {
1433
1433
  opening = new Promise((resolve, reject) => {
1434
1434
  let ws;
1435
1435
  let self = null;
1436
- const fail = (err) => {
1436
+ const fail2 = (err) => {
1437
1437
  clearTimeout(openTimer);
1438
1438
  if (self) {
1439
1439
  self.retired = true;
@@ -1455,12 +1455,12 @@ function getOrOpen(url, token) {
1455
1455
  ws?.close();
1456
1456
  } catch {
1457
1457
  }
1458
- fail(new DbWsTransportError("ws open timed out"));
1458
+ fail2(new DbWsTransportError("ws open timed out"));
1459
1459
  }, WS_OPEN_TIMEOUT_MS);
1460
1460
  try {
1461
1461
  ws = new WebSocketCtor(url, ["auth", token, DB_WS_SUBPROTOCOL]);
1462
1462
  } catch (err) {
1463
- fail(new DbWsTransportError(err?.message || "ws construct failed"));
1463
+ fail2(new DbWsTransportError(err?.message || "ws construct failed"));
1464
1464
  return;
1465
1465
  }
1466
1466
  self = {
@@ -1536,8 +1536,8 @@ function getOrOpen(url, token) {
1536
1536
  deliver(c, msg);
1537
1537
  }
1538
1538
  };
1539
- ws.onerror = () => fail(new DbWsTransportError("ws error"));
1540
- ws.onclose = () => fail(new DbWsTransportError("ws closed"));
1539
+ ws.onerror = () => fail2(new DbWsTransportError("ws error"));
1540
+ ws.onclose = () => fail2(new DbWsTransportError("ws closed"));
1541
1541
  });
1542
1542
  return opening;
1543
1543
  }
@@ -1829,35 +1829,33 @@ function serializeParam(val) {
1829
1829
  return JSON.stringify(val);
1830
1830
  }
1831
1831
  function serializeColumnParam(val, columnName, columns) {
1832
- const col = columns.find((c) => c.name === columnName);
1832
+ const col = columnsByName(columns).get(columnName);
1833
1833
  if (col?.type === "user" && typeof val === "string") {
1834
1834
  return val.startsWith(USER_PREFIX) ? val : `${USER_PREFIX}${val}`;
1835
1835
  }
1836
1836
  return serializeParam(val);
1837
1837
  }
1838
- function escapeValue(val) {
1839
- if (val === null || val === void 0) {
1840
- return "NULL";
1841
- }
1842
- if (typeof val === "boolean") {
1843
- return val ? "1" : "0";
1844
- }
1845
- if (typeof val === "number") {
1846
- return String(val);
1847
- }
1848
- if (typeof val === "string") {
1849
- return `'${val.replace(/'/g, "''")}'`;
1838
+ function columnsByName(columns) {
1839
+ let byName = byNameCache.get(columns);
1840
+ if (!byName) {
1841
+ byName = /* @__PURE__ */ new Map();
1842
+ for (const column of columns) {
1843
+ if (!byName.has(column.name)) {
1844
+ byName.set(column.name, column);
1845
+ }
1846
+ }
1847
+ byNameCache.set(columns, byName);
1850
1848
  }
1851
- const json = JSON.stringify(val);
1852
- return `'${json.replace(/'/g, "''")}'`;
1849
+ return byName;
1853
1850
  }
1854
1851
  function deserializeRow(row, columns) {
1855
1852
  if (row == null) {
1856
1853
  return row;
1857
1854
  }
1858
1855
  const result = {};
1856
+ const byName = columnsByName(columns);
1859
1857
  for (const [key, value] of Object.entries(row)) {
1860
- const col = columns.find((c) => c.name === key);
1858
+ const col = byName.get(key);
1861
1859
  if (typeof value === "string" && value.startsWith(USER_PREFIX)) {
1862
1860
  result[key] = value.slice(USER_PREFIX.length);
1863
1861
  } else if (col?.type === "json" && typeof value === "string") {
@@ -1883,8 +1881,50 @@ function deserializeRow(row, columns) {
1883
1881
  }
1884
1882
  return result;
1885
1883
  }
1884
+ function storageClass(value) {
1885
+ if (value === null || value === void 0) {
1886
+ return 0;
1887
+ }
1888
+ if (typeof value === "number" || typeof value === "boolean") {
1889
+ return 1;
1890
+ }
1891
+ if (value instanceof Uint8Array) {
1892
+ return 3;
1893
+ }
1894
+ return 2;
1895
+ }
1896
+ function compareAsSqlite(a, b) {
1897
+ const classA = storageClass(a);
1898
+ const classB = storageClass(b);
1899
+ if (classA !== classB) {
1900
+ return classA - classB;
1901
+ }
1902
+ if (classA === 1) {
1903
+ return Number(a) - Number(b);
1904
+ }
1905
+ if (classA === 2) {
1906
+ const textA = typeof a === "string" ? a : JSON.stringify(a);
1907
+ const textB = typeof b === "string" ? b : JSON.stringify(b);
1908
+ return textA < textB ? -1 : textA > textB ? 1 : 0;
1909
+ }
1910
+ if (classA === 3) {
1911
+ const bytesA = a;
1912
+ const bytesB = b;
1913
+ for (let i = 0; i < Math.min(bytesA.length, bytesB.length); i++) {
1914
+ if (bytesA[i] !== bytesB[i]) {
1915
+ return bytesA[i] - bytesB[i];
1916
+ }
1917
+ }
1918
+ return bytesA.length - bytesB.length;
1919
+ }
1920
+ return 0;
1921
+ }
1886
1922
  function buildSelect(table, options = {}) {
1887
- let sql = `SELECT * FROM ${table}`;
1923
+ for (const column of options.columns ?? []) {
1924
+ assertSqlIdentifier(column, "selected column");
1925
+ }
1926
+ const selected = options.columns?.length ? options.columns.join(", ") : "*";
1927
+ let sql = `SELECT ${selected} FROM ${table}`;
1888
1928
  const params = [];
1889
1929
  if (options.where) {
1890
1930
  sql += ` WHERE ${options.where}`;
@@ -1895,11 +1935,13 @@ function buildSelect(table, options = {}) {
1895
1935
  if (options.orderBy) {
1896
1936
  sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
1897
1937
  }
1898
- if (options.limit != null) {
1899
- sql += ` LIMIT ${options.limit}`;
1938
+ if (options.limit != null || options.offset != null) {
1939
+ sql += " LIMIT ?";
1940
+ params.push(options.limit ?? -1);
1900
1941
  }
1901
1942
  if (options.offset != null) {
1902
- sql += ` OFFSET ${options.offset}`;
1943
+ sql += " OFFSET ?";
1944
+ params.push(options.offset);
1903
1945
  }
1904
1946
  return { sql, params: params.length > 0 ? params : void 0 };
1905
1947
  }
@@ -1967,13 +2009,12 @@ function buildGroupedAggregate(table, options) {
1967
2009
  if (options.orderBy) {
1968
2010
  sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
1969
2011
  }
2012
+ const params = [...options.whereParams ?? []];
1970
2013
  if (options.limit != null) {
1971
- sql += ` LIMIT ${options.limit}`;
2014
+ sql += " LIMIT ?";
2015
+ params.push(options.limit);
1972
2016
  }
1973
- return {
1974
- sql,
1975
- params: options.whereParams?.length ? options.whereParams : void 0
1976
- };
2017
+ return { sql, params: params.length > 0 ? params : void 0 };
1977
2018
  }
1978
2019
  function buildExists(table, where, whereParams, negate) {
1979
2020
  const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
@@ -2033,12 +2074,13 @@ function stripSystemColumns(data) {
2033
2074
  }
2034
2075
  return result;
2035
2076
  }
2036
- var USER_PREFIX, SQL_IDENTIFIER_RE, SYSTEM_COLUMNS;
2077
+ var USER_PREFIX, byNameCache, SQL_IDENTIFIER_RE, SYSTEM_COLUMNS;
2037
2078
  var init_sql = __esm({
2038
2079
  "src/db/sql.ts"() {
2039
2080
  "use strict";
2040
2081
  init_errors();
2041
2082
  USER_PREFIX = "@@user@@";
2083
+ byNameCache = /* @__PURE__ */ new WeakMap();
2042
2084
  SQL_IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
2043
2085
  SYSTEM_COLUMNS = /* @__PURE__ */ new Set([
2044
2086
  "id",
@@ -2052,6 +2094,22 @@ var init_sql = __esm({
2052
2094
  }
2053
2095
  });
2054
2096
 
2097
+ // src/db/time.ts
2098
+ var timeHelpers;
2099
+ var init_time = __esm({
2100
+ "src/db/time.ts"() {
2101
+ "use strict";
2102
+ timeHelpers = {
2103
+ now: () => Date.now(),
2104
+ days: (n) => n * 864e5,
2105
+ hours: (n) => n * 36e5,
2106
+ minutes: (n) => n * 6e4,
2107
+ ago: (ms) => Date.now() - ms,
2108
+ fromNow: (ms) => Date.now() + ms
2109
+ };
2110
+ }
2111
+ });
2112
+
2055
2113
  // src/db/predicate.ts
2056
2114
  function compilePredicate(fn, bindings, columns = []) {
2057
2115
  try {
@@ -2066,9 +2124,9 @@ function compilePredicate(fn, bindings, columns = []) {
2066
2124
  }
2067
2125
  const tokens = tokenize(body);
2068
2126
  if (tokens.length === 0) {
2069
- return { type: "js", fn, reason: "empty token stream" };
2127
+ return { type: "js", fn, reason: "unsupported syntax" };
2070
2128
  }
2071
- const parser = new Parser(tokens, names.row, names.bindings, bindings, fn);
2129
+ const parser = new Parser(tokens, names.row, names.bindings, bindings);
2072
2130
  const ast = parser.parseExpression();
2073
2131
  if (!ast) {
2074
2132
  return { type: "js", fn, reason: "could not parse expression" };
@@ -2076,11 +2134,12 @@ function compilePredicate(fn, bindings, columns = []) {
2076
2134
  if (parser.pos < tokens.length) {
2077
2135
  return { type: "js", fn, reason: "unexpected tokens after expression" };
2078
2136
  }
2079
- const where = compileNode(ast, columns);
2137
+ const params = [];
2138
+ const where = compileNode(ast, false, columns, params);
2080
2139
  if (!where) {
2081
2140
  return { type: "js", fn, reason: "could not compile to SQL" };
2082
2141
  }
2083
- return { type: "sql", where };
2142
+ return { type: "sql", where, params };
2084
2143
  } catch (err) {
2085
2144
  return {
2086
2145
  type: "js",
@@ -2163,188 +2222,408 @@ function extractBody(source) {
2163
2222
  function tokenize(expr) {
2164
2223
  const tokens = [];
2165
2224
  let i = 0;
2166
- while (i < expr.length) {
2167
- const ch = expr[i];
2168
- if (/\s/.test(ch)) {
2169
- i++;
2170
- continue;
2171
- }
2172
- if (ch === "'" || ch === '"') {
2173
- const quote = ch;
2174
- let str = "";
2175
- i++;
2176
- while (i < expr.length && expr[i] !== quote) {
2177
- if (expr[i] === "\\") {
2178
- i++;
2179
- if (i < expr.length) {
2180
- str += expr[i];
2181
- }
2182
- } else {
2183
- str += expr[i];
2184
- }
2225
+ try {
2226
+ while (i < expr.length) {
2227
+ const ch = expr[i];
2228
+ if (/\s/.test(ch)) {
2185
2229
  i++;
2230
+ continue;
2231
+ }
2232
+ if (ch === "'" || ch === '"' || ch === "`") {
2233
+ const read = readString(expr, i);
2234
+ tokens.push({ type: "string", value: read.value });
2235
+ i = read.end;
2236
+ continue;
2186
2237
  }
2187
- if (i >= expr.length) {
2188
- return [];
2238
+ if (/[0-9]/.test(ch) || ch === "." && /[0-9]/.test(expr[i + 1] ?? "")) {
2239
+ const read = readNumber(expr, i);
2240
+ tokens.push({ type: "number", value: read.text, number: read.value });
2241
+ i = read.end;
2242
+ continue;
2189
2243
  }
2190
- i++;
2191
- tokens.push({ type: "string", value: str });
2192
- continue;
2193
- }
2194
- if (ch === "`") {
2195
- return [];
2196
- }
2197
- if (/[0-9]/.test(ch) || ch === "-" && i + 1 < expr.length && /[0-9]/.test(expr[i + 1])) {
2198
- let num = ch;
2199
- i++;
2200
- while (i < expr.length && /[0-9.]/.test(expr[i])) {
2201
- num += expr[i];
2244
+ const op = OPERATORS.find((o) => expr.startsWith(o, i));
2245
+ if (op) {
2246
+ tokens.push({ type: "operator", value: op });
2247
+ i += op.length;
2248
+ continue;
2249
+ }
2250
+ if (PUNCTUATION[ch]) {
2251
+ tokens.push({ type: PUNCTUATION[ch], value: ch });
2202
2252
  i++;
2253
+ continue;
2203
2254
  }
2204
- tokens.push({ type: "number", value: num });
2205
- continue;
2206
- }
2207
- if (expr.slice(i, i + 3) === "===" || expr.slice(i, i + 3) === "!==") {
2208
- tokens.push({ type: "operator", value: expr.slice(i, i + 3) });
2209
- i += 3;
2210
- continue;
2211
- }
2212
- if (expr.slice(i, i + 2) === "==" || expr.slice(i, i + 2) === "!=" || expr.slice(i, i + 2) === "<=" || expr.slice(i, i + 2) === ">=" || expr.slice(i, i + 2) === "&&" || expr.slice(i, i + 2) === "||") {
2213
- tokens.push({ type: "operator", value: expr.slice(i, i + 2) });
2214
- i += 2;
2215
- continue;
2255
+ if (/[a-zA-Z_$]/.test(ch)) {
2256
+ let ident = ch;
2257
+ i++;
2258
+ while (i < expr.length && /[a-zA-Z0-9_$]/.test(expr[i])) {
2259
+ ident += expr[i];
2260
+ i++;
2261
+ }
2262
+ tokens.push({ type: "identifier", value: ident });
2263
+ continue;
2264
+ }
2265
+ return [];
2216
2266
  }
2217
- if (ch === "!" || ch === "<" || ch === ">") {
2218
- tokens.push({ type: "operator", value: ch });
2219
- i++;
2220
- continue;
2267
+ } catch (err) {
2268
+ if (err instanceof Unreadable) {
2269
+ return [];
2221
2270
  }
2222
- if (ch === ".") {
2223
- tokens.push({ type: "dot", value: "." });
2224
- i++;
2225
- continue;
2271
+ throw err;
2272
+ }
2273
+ return tokens;
2274
+ }
2275
+ function readString(expr, start) {
2276
+ const quote = expr[start];
2277
+ let value = "";
2278
+ let i = start + 1;
2279
+ while (i < expr.length && expr[i] !== quote) {
2280
+ const ch = expr[i];
2281
+ if (quote === "`" && ch === "$" && expr[i + 1] === "{") {
2282
+ throw new Unreadable("template substitution");
2226
2283
  }
2227
- if (ch === "(") {
2228
- tokens.push({ type: "lparen", value: "(" });
2284
+ if (ch !== "\\") {
2285
+ value += ch;
2229
2286
  i++;
2230
2287
  continue;
2231
2288
  }
2232
- if (ch === ")") {
2233
- tokens.push({ type: "rparen", value: ")" });
2234
- i++;
2235
- continue;
2289
+ const escaped = readEscape(expr, i + 1);
2290
+ value += escaped.value;
2291
+ i = escaped.end;
2292
+ }
2293
+ if (i >= expr.length) {
2294
+ throw new Unreadable("unterminated string");
2295
+ }
2296
+ return { value, end: i + 1 };
2297
+ }
2298
+ function readEscape(expr, start) {
2299
+ const ch = expr[start];
2300
+ if (ch === void 0) {
2301
+ throw new Unreadable("unterminated escape");
2302
+ }
2303
+ if (ch in SINGLE_ESCAPES) {
2304
+ return { value: SINGLE_ESCAPES[ch], end: start + 1 };
2305
+ }
2306
+ if (ch === "0" && !/[0-9]/.test(expr[start + 1] ?? "")) {
2307
+ return { value: "\0", end: start + 1 };
2308
+ }
2309
+ if (/[0-9]/.test(ch)) {
2310
+ throw new Unreadable("octal escape");
2311
+ }
2312
+ if (ch === "x") {
2313
+ const hex = expr.slice(start + 1, start + 3);
2314
+ if (!/^[0-9a-fA-F]{2}$/.test(hex)) {
2315
+ throw new Unreadable("bad \\x escape");
2236
2316
  }
2237
- if (ch === "[") {
2238
- tokens.push({ type: "lbracket", value: "[" });
2239
- i++;
2240
- continue;
2317
+ return { value: String.fromCharCode(parseInt(hex, 16)), end: start + 3 };
2318
+ }
2319
+ if (ch === "u") {
2320
+ if (expr[start + 1] === "{") {
2321
+ const close = expr.indexOf("}", start + 2);
2322
+ const hex2 = close === -1 ? "" : expr.slice(start + 2, close);
2323
+ const code = parseInt(hex2, 16);
2324
+ if (!/^[0-9a-fA-F]{1,6}$/.test(hex2) || code > 1114111) {
2325
+ throw new Unreadable("bad \\u{} escape");
2326
+ }
2327
+ return { value: String.fromCodePoint(code), end: close + 1 };
2241
2328
  }
2242
- if (ch === "]") {
2243
- tokens.push({ type: "rbracket", value: "]" });
2244
- i++;
2245
- continue;
2329
+ const hex = expr.slice(start + 1, start + 5);
2330
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
2331
+ throw new Unreadable("bad \\u escape");
2246
2332
  }
2247
- if (ch === ",") {
2248
- tokens.push({ type: "comma", value: "," });
2249
- i++;
2250
- continue;
2333
+ return { value: String.fromCharCode(parseInt(hex, 16)), end: start + 5 };
2334
+ }
2335
+ if (ch === "\r" && expr[start + 1] === "\n") {
2336
+ return { value: "", end: start + 2 };
2337
+ }
2338
+ if (ch === "\n" || ch === "\r" || ch === "\u2028" || ch === "\u2029") {
2339
+ return { value: "", end: start + 1 };
2340
+ }
2341
+ return { value: ch, end: start + 1 };
2342
+ }
2343
+ function readNumber(expr, start) {
2344
+ const match = /^0[xX][0-9a-fA-F_]+|^0[oO][0-7_]+|^0[bB][01_]+|^(?:[0-9][0-9_]*)?\.?[0-9_]*(?:[eE][+-]?[0-9_]+)?/.exec(
2345
+ expr.slice(start)
2346
+ );
2347
+ const text = match?.[0] ?? "";
2348
+ const end = start + text.length;
2349
+ if (!text || /[a-zA-Z0-9_$]/.test(expr[end] ?? "")) {
2350
+ throw new Unreadable("unsupported number");
2351
+ }
2352
+ const value = Number(text.replace(/_/g, ""));
2353
+ if (!Number.isFinite(value)) {
2354
+ throw new Unreadable("non-finite number");
2355
+ }
2356
+ return { text, value, end };
2357
+ }
2358
+ function comparisonNode(field, op, value) {
2359
+ if (value === null || value === void 0) {
2360
+ if (op === "===" || op === "==" || op === "!==" || op === "!=") {
2361
+ return {
2362
+ kind: "nullCheck",
2363
+ field,
2364
+ strict: op === "===" || op === "!==",
2365
+ isNull: op === "===" || op === "=="
2366
+ };
2251
2367
  }
2252
- if (/[a-zA-Z_$]/.test(ch)) {
2253
- let ident = ch;
2254
- i++;
2255
- while (i < expr.length && /[a-zA-Z0-9_$]/.test(expr[i])) {
2256
- ident += expr[i];
2257
- i++;
2258
- }
2259
- tokens.push({ type: "identifier", value: ident });
2260
- continue;
2368
+ return null;
2369
+ }
2370
+ if (value instanceof Date) {
2371
+ if (op === "===" || op === "==" || op === "!==" || op === "!=") {
2372
+ return null;
2261
2373
  }
2262
- return [];
2374
+ return comparisonNode(field, op, value.getTime());
2263
2375
  }
2264
- return tokens;
2376
+ if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
2377
+ return { kind: "comparison", field, op, value };
2378
+ }
2379
+ return null;
2265
2380
  }
2266
- function compileNode(node, columns) {
2267
- const stored = (field, value) => escapeValue(serializeColumnParam(value, field, columns));
2381
+ function jsCompare(a, op, b) {
2382
+ const x = a;
2383
+ const y = b;
2384
+ switch (op) {
2385
+ case "===":
2386
+ return x === y;
2387
+ case "!==":
2388
+ return x !== y;
2389
+ case "==":
2390
+ return x == y;
2391
+ case "!=":
2392
+ return x != y;
2393
+ case "<":
2394
+ return x < y;
2395
+ case ">":
2396
+ return x > y;
2397
+ case "<=":
2398
+ return x <= y;
2399
+ case ">=":
2400
+ return x >= y;
2401
+ }
2402
+ }
2403
+ function nestedComparison(node, negated, params) {
2404
+ const strictEquals = node.op === "===" ? !negated : node.op === "!==" ? negated : null;
2405
+ const types = JSON_TYPES[typeof node.value];
2406
+ if (strictEquals === null || !types) {
2407
+ return null;
2408
+ }
2409
+ params.push(node.value);
2410
+ const equals = `(json_type(${node.field.column}, '${node.field.path}') IN ${types} AND ${node.field.sql} = ?)`;
2411
+ return strictEquals ? equals : `NOT coalesce(${equals}, 0)`;
2412
+ }
2413
+ function withNulls(core, field, nullsMatch) {
2414
+ return nullsMatch ? `(${core} OR ${field.sql} IS NULL)` : core;
2415
+ }
2416
+ function readsBackAsWritten(value) {
2417
+ if (typeof value !== "string") {
2418
+ return false;
2419
+ }
2420
+ try {
2421
+ JSON.parse(value);
2422
+ return false;
2423
+ } catch {
2424
+ return true;
2425
+ }
2426
+ }
2427
+ function jsonColumnIn(field, values, negated, params) {
2428
+ params.push(JSON.stringify(values.flatMap((v) => [v, JSON.stringify(v)])));
2429
+ return withNulls(
2430
+ `${field.sql} ${negated ? "NOT IN" : "IN"} (SELECT value FROM json_each(?))`,
2431
+ field,
2432
+ negated
2433
+ );
2434
+ }
2435
+ function compileNode(node, negated, columns, params) {
2436
+ const column = (field) => field.nested ? void 0 : columns.find((c) => c.name === field.column);
2437
+ const stored = (field, value) => field.nested ? serializeColumnParam(value, "", []) : serializeColumnParam(value, field.column, columns);
2268
2438
  switch (node.kind) {
2269
- case "comparison":
2270
- return `${node.field} ${node.operator} ${stored(node.field, node.value)}`;
2271
- case "nullCheck":
2272
- return `${node.field} ${node.isNull ? "IS NULL" : "IS NOT NULL"}`;
2273
- case "in": {
2274
- if (node.values.length === 0) {
2275
- return "0";
2276
- }
2277
- const vals = node.values.map((v) => stored(node.field, v)).join(", ");
2278
- return `${node.field} IN (${vals})`;
2279
- }
2280
- case "like":
2281
- return `${node.field} LIKE ${escapeValue(node.pattern)}`;
2282
- case "booleanField":
2283
- return node.negated ? `${node.field} = 0` : `${node.field} = 1`;
2284
2439
  case "logical": {
2285
- const left = compileNode(node.left, columns);
2286
- const right = compileNode(node.right, columns);
2287
- if (!left || !right) {
2440
+ const operator = negated ? node.operator === "AND" ? "OR" : "AND" : node.operator;
2441
+ const left = compileNode(node.left, negated, columns, params);
2442
+ const right = left && compileNode(node.right, negated, columns, params);
2443
+ return left && right ? `(${left} ${operator} ${right})` : null;
2444
+ }
2445
+ case "not":
2446
+ return compileNode(node.operand, !negated, columns, params);
2447
+ case "comparison": {
2448
+ if (node.field.nested) {
2449
+ return nestedComparison(node, negated, params);
2450
+ }
2451
+ const type = column(node.field)?.type;
2452
+ if (type === "json") {
2453
+ const strictEquals = node.op === "===" ? !negated : node.op === "!==" ? negated : null;
2454
+ return strictEquals !== null && readsBackAsWritten(node.value) ? jsonColumnIn(node.field, [node.value], !strictEquals, params) : null;
2455
+ }
2456
+ if (!type || typeof node.value !== VALUE_KIND[type]) {
2288
2457
  return null;
2289
2458
  }
2290
- return `(${left} ${node.operator} ${right})`;
2459
+ const op = negated ? NEGATED[node.op] : node.op;
2460
+ const nullsMatch = negated ? !jsCompare(null, node.op, node.value) : jsCompare(null, node.op, node.value);
2461
+ params.push(stored(node.field, node.value));
2462
+ if (SQL_OP[op] === "!=" && nullsMatch) {
2463
+ return `${node.field.sql} IS NOT ?`;
2464
+ }
2465
+ return withNulls(
2466
+ `${node.field.sql} ${SQL_OP[op]} ?`,
2467
+ node.field,
2468
+ nullsMatch
2469
+ );
2291
2470
  }
2292
- case "not": {
2293
- const inner = compileNode(node.operand, columns);
2294
- if (!inner) {
2471
+ case "nullCheck": {
2472
+ if (node.strict && node.field.nested) {
2295
2473
  return null;
2296
2474
  }
2297
- return `NOT (${inner})`;
2475
+ const isNull = negated ? !node.isNull : node.isNull;
2476
+ if (column(node.field)?.type === "json") {
2477
+ return isNull ? `(${node.field.sql} IS NULL OR ${node.field.sql} = 'null')` : `(${node.field.sql} IS NOT NULL AND ${node.field.sql} <> 'null')`;
2478
+ }
2479
+ return `${node.field.sql} ${isNull ? "IS NULL" : "IS NOT NULL"}`;
2298
2480
  }
2299
- default:
2481
+ case "in": {
2482
+ const type = column(node.field)?.type;
2483
+ if (type === "json") {
2484
+ return node.values.every(readsBackAsWritten) ? jsonColumnIn(node.field, node.values, negated, params) : null;
2485
+ }
2486
+ if (!type || node.values.some(
2487
+ (v) => v !== null && v !== void 0 && typeof v !== VALUE_KIND[type]
2488
+ )) {
2489
+ return null;
2490
+ }
2491
+ const listed = node.values.filter((v) => v !== null && v !== void 0);
2492
+ const nullsMatch = negated ? !node.values.includes(null) : node.values.includes(null);
2493
+ let core;
2494
+ if (listed.length === 0) {
2495
+ core = negated ? `${node.field.sql} IS NOT NULL` : "0";
2496
+ } else {
2497
+ params.push(JSON.stringify(listed.map((v) => stored(node.field, v))));
2498
+ core = `${node.field.sql} ${negated ? "NOT IN" : "IN"} (SELECT value FROM json_each(?))`;
2499
+ }
2500
+ return withNulls(core, node.field, nullsMatch);
2501
+ }
2502
+ case "includes": {
2503
+ const type = column(node.field)?.type;
2504
+ if (type === "json") {
2505
+ const col = node.field.sql;
2506
+ let element;
2507
+ if (typeof node.needle === "boolean") {
2508
+ element = `type = '${node.needle}'`;
2509
+ } else {
2510
+ params.push(node.needle);
2511
+ element = `type IN ${JSON_TYPES[typeof node.needle]} AND value = ?`;
2512
+ }
2513
+ const substring = `${negated ? "=" : ">"} 0`;
2514
+ params.push(String(node.needle), String(node.needle));
2515
+ return `(CASE WHEN json_valid(${col}) THEN CASE json_type(${col}) WHEN 'array' THEN ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${col}) WHERE ${element}) WHEN 'text' THEN instr(${col} ->> '$', ?) ${substring} ELSE 0 END ELSE instr(${col}, ?) ${substring} END)`;
2516
+ }
2517
+ if ((type === "text" || type === "user") && typeof node.needle === "string") {
2518
+ params.push(node.needle);
2519
+ return `instr(${node.field.sql}, ?) ${negated ? "=" : ">"} 0`;
2520
+ }
2300
2521
  return null;
2522
+ }
2523
+ case "truthy": {
2524
+ const type = column(node.field)?.type;
2525
+ if (node.field.nested) {
2526
+ return negated ? `(${node.field.sql} IN (0, '') OR ${node.field.sql} IS NULL)` : `${node.field.sql} NOT IN (0, '')`;
2527
+ }
2528
+ switch (type) {
2529
+ case "boolean":
2530
+ return negated ? `${node.field.sql} IS NOT 1` : `${node.field.sql} = 1`;
2531
+ case "number":
2532
+ return negated ? `(${node.field.sql} = 0 OR ${node.field.sql} IS NULL)` : `${node.field.sql} <> 0`;
2533
+ case "text":
2534
+ case "user":
2535
+ return negated ? `(${node.field.sql} = '' OR ${node.field.sql} IS NULL)` : `${node.field.sql} <> ''`;
2536
+ default:
2537
+ return null;
2538
+ }
2539
+ }
2301
2540
  }
2302
2541
  }
2303
- function isComparisonOp(value) {
2304
- return value in JS_TO_SQL_OP;
2305
- }
2306
- var Parser, JS_TO_SQL_OP, PARSE_FAILED;
2542
+ var OPERATORS, PUNCTUATION, Unreadable, SINGLE_ESCAPES, PARSE_FAILED, COMPARISON_OPS, FLIPPED, Parser, NEGATED, SQL_OP, VALUE_KIND, JSON_TYPES;
2307
2543
  var init_predicate = __esm({
2308
2544
  "src/db/predicate.ts"() {
2309
2545
  "use strict";
2310
2546
  init_sql();
2547
+ init_time();
2548
+ OPERATORS = [
2549
+ "===",
2550
+ "!==",
2551
+ "==",
2552
+ "!=",
2553
+ "<=",
2554
+ ">=",
2555
+ "&&",
2556
+ "||",
2557
+ "!",
2558
+ "<",
2559
+ ">",
2560
+ "+",
2561
+ "-",
2562
+ "*",
2563
+ "/",
2564
+ "%"
2565
+ ];
2566
+ PUNCTUATION = {
2567
+ ".": "dot",
2568
+ "(": "lparen",
2569
+ ")": "rparen",
2570
+ "[": "lbracket",
2571
+ "]": "rbracket",
2572
+ ",": "comma"
2573
+ };
2574
+ Unreadable = class extends Error {
2575
+ };
2576
+ SINGLE_ESCAPES = {
2577
+ n: "\n",
2578
+ r: "\r",
2579
+ t: " ",
2580
+ b: "\b",
2581
+ f: "\f",
2582
+ v: "\v"
2583
+ };
2584
+ PARSE_FAILED = /* @__PURE__ */ Symbol("PARSE_FAILED");
2585
+ COMPARISON_OPS = /* @__PURE__ */ new Set([
2586
+ "===",
2587
+ "!==",
2588
+ "==",
2589
+ "!=",
2590
+ "<",
2591
+ ">",
2592
+ "<=",
2593
+ ">="
2594
+ ]);
2595
+ FLIPPED = {
2596
+ "===": "===",
2597
+ "!==": "!==",
2598
+ "==": "==",
2599
+ "!=": "!=",
2600
+ "<": ">",
2601
+ ">": "<",
2602
+ "<=": ">=",
2603
+ ">=": "<="
2604
+ };
2311
2605
  Parser = class {
2312
- constructor(tokens, paramName, bindingsName, bindingsValue, originalFn) {
2606
+ constructor(tokens, paramName, bindingsName, bindingsValue) {
2313
2607
  this.tokens = tokens;
2314
2608
  this.paramName = paramName;
2315
2609
  this.bindingsName = bindingsName;
2316
2610
  this.bindingsValue = bindingsValue;
2317
- this.originalFn = originalFn;
2318
2611
  }
2319
2612
  tokens;
2320
2613
  paramName;
2321
2614
  bindingsName;
2322
2615
  bindingsValue;
2323
- originalFn;
2324
2616
  pos = 0;
2325
- /** Peek at the current token without consuming it. */
2326
- peek() {
2327
- return this.tokens[this.pos];
2617
+ peek(offset = 0) {
2618
+ return this.tokens[this.pos + offset];
2328
2619
  }
2329
- /** Consume the current token and advance. */
2330
2620
  advance() {
2331
2621
  return this.tokens[this.pos++];
2332
2622
  }
2333
- /** Check if the current token matches an expected type and value. */
2334
- match(type, value) {
2335
- const t = this.peek();
2336
- if (!t) {
2337
- return false;
2338
- }
2339
- if (t.type !== type) {
2340
- return false;
2341
- }
2342
- if (value !== void 0 && t.value !== value) {
2343
- return false;
2344
- }
2345
- return true;
2623
+ match(type, value, offset = 0) {
2624
+ const t = this.peek(offset);
2625
+ return !!t && t.type === type && (value === void 0 || t.value === value);
2346
2626
  }
2347
- /** Consume a token if it matches, otherwise return false. */
2348
2627
  eat(type, value) {
2349
2628
  if (this.match(type, value)) {
2350
2629
  this.advance();
@@ -2353,18 +2632,15 @@ var init_predicate = __esm({
2353
2632
  return false;
2354
2633
  }
2355
2634
  // --- Grammar rules ---
2356
- /** Entry point: parse a full expression. */
2357
2635
  parseExpression() {
2358
2636
  return this.parseOr();
2359
2637
  }
2360
- /** or_expr → and_expr ( '||' and_expr )* */
2361
2638
  parseOr() {
2362
2639
  let left = this.parseAnd();
2363
2640
  if (!left) {
2364
2641
  return null;
2365
2642
  }
2366
- while (this.match("operator", "||")) {
2367
- this.advance();
2643
+ while (this.eat("operator", "||")) {
2368
2644
  const right = this.parseAnd();
2369
2645
  if (!right) {
2370
2646
  return null;
@@ -2373,14 +2649,12 @@ var init_predicate = __esm({
2373
2649
  }
2374
2650
  return left;
2375
2651
  }
2376
- /** and_expr → not_expr ( '&&' not_expr )* */
2377
2652
  parseAnd() {
2378
2653
  let left = this.parseNot();
2379
2654
  if (!left) {
2380
2655
  return null;
2381
2656
  }
2382
- while (this.match("operator", "&&")) {
2383
- this.advance();
2657
+ while (this.eat("operator", "&&")) {
2384
2658
  const right = this.parseNot();
2385
2659
  if (!right) {
2386
2660
  return null;
@@ -2389,72 +2663,48 @@ var init_predicate = __esm({
2389
2663
  }
2390
2664
  return left;
2391
2665
  }
2392
- /** not_expr → '!' not_expr | primary */
2393
2666
  parseNot() {
2394
- if (this.match("operator", "!")) {
2395
- this.advance();
2396
- if (this.match("lparen")) {
2397
- this.advance();
2398
- const inner2 = this.parseExpression();
2399
- if (!inner2) {
2400
- return null;
2401
- }
2402
- if (!this.eat("rparen")) {
2403
- return null;
2404
- }
2405
- return { kind: "not", operand: inner2 };
2406
- }
2407
- const inner = this.parsePrimary();
2408
- if (!inner) {
2409
- return null;
2410
- }
2411
- if (inner.kind === "booleanField") {
2412
- return { ...inner, negated: !inner.negated };
2413
- }
2414
- return { kind: "not", operand: inner };
2667
+ if (this.eat("operator", "!")) {
2668
+ const operand = this.parseNot();
2669
+ return operand ? { kind: "not", operand } : null;
2415
2670
  }
2416
2671
  return this.parsePrimary();
2417
2672
  }
2418
- /**
2419
- * primary → field_comparison | null_check | includes_expr | paren_expr | boolean_field
2420
- *
2421
- * This is the workhorse — handles the different patterns that can appear
2422
- * as atomic expressions within a larger &&/|| combination.
2423
- */
2424
2673
  parsePrimary() {
2425
- if (this.match("lparen")) {
2426
- this.advance();
2674
+ if (this.eat("lparen")) {
2427
2675
  const inner = this.parseExpression();
2428
- if (!inner) {
2429
- return null;
2430
- }
2431
- if (!this.eat("rparen")) {
2676
+ if (!inner || !this.eat("rparen")) {
2432
2677
  return null;
2433
2678
  }
2434
2679
  return inner;
2435
2680
  }
2436
- if (this.match("lbracket")) {
2437
- return this.parseArrayIncludes();
2438
- }
2439
2681
  if (this.match("identifier", this.paramName)) {
2440
2682
  return this.parseFieldExpression();
2441
2683
  }
2442
- if (this.bindingsName && this.match("identifier", this.bindingsName)) {
2443
- return this.parseBindingsArrayIncludes();
2684
+ const membership = this.tryParseMembership();
2685
+ if (membership) {
2686
+ return membership;
2444
2687
  }
2445
- if (this.match("identifier")) {
2446
- return this.parseNonParamExpression();
2688
+ const start = this.pos;
2689
+ const value = this.parseValue();
2690
+ if (value !== PARSE_FAILED && this.match("operator")) {
2691
+ const op = this.peek().value;
2692
+ if (COMPARISON_OPS.has(op) && this.match("identifier", this.paramName, 1)) {
2693
+ this.advance();
2694
+ this.advance();
2695
+ const field = this.parseFieldPath();
2696
+ if (field && !this.lookAheadForIncludes()) {
2697
+ return comparisonNode(field, FLIPPED[op], value);
2698
+ }
2699
+ }
2447
2700
  }
2701
+ this.pos = start;
2448
2702
  return null;
2449
2703
  }
2450
2704
  /**
2451
- * Parse an expression that starts with the parameter name (e.g. `o.field`).
2452
- *
2453
- * Could be:
2454
- * - `o.field === value` (comparison)
2455
- * - `o.field != null` (null check)
2456
- * - `o.field.includes('text')` (LIKE)
2457
- * - `o.field` alone (boolean field check)
2705
+ * An expression starting with the row parameter: a comparison
2706
+ * (`o.field === value`), `o.field.includes(x)`, or the field alone, read
2707
+ * for its truthiness.
2458
2708
  */
2459
2709
  parseFieldExpression() {
2460
2710
  this.advance();
@@ -2462,236 +2712,185 @@ var init_predicate = __esm({
2462
2712
  if (!field) {
2463
2713
  return null;
2464
2714
  }
2465
- const next = this.peek();
2466
- if (next?.type === "dot" && this.lookAheadForIncludes()) {
2715
+ if (this.lookAheadForIncludes()) {
2467
2716
  return this.parseFieldIncludes(field);
2468
2717
  }
2469
- if (next?.type === "operator" && isComparisonOp(next.value)) {
2470
- return this.parseComparison(field);
2718
+ const next = this.peek();
2719
+ if (next?.type === "operator" && COMPARISON_OPS.has(next.value)) {
2720
+ this.advance();
2721
+ const value = this.parseValue();
2722
+ if (value === PARSE_FAILED) {
2723
+ return null;
2724
+ }
2725
+ return comparisonNode(field, next.value, value);
2471
2726
  }
2472
- return { kind: "booleanField", field, negated: false };
2727
+ return { kind: "truthy", field };
2473
2728
  }
2474
2729
  /**
2475
- * Parse a dot-separated field path after the parameter name.
2476
- * `o.status` → `"status"`, `o.address.city` → `"json_extract(address, '$.city')"`.
2730
+ * The field path after the row parameter: `.status` → the column,
2731
+ * `.address.city` → `json_extract(address, '$.city')`. Stops before a
2732
+ * method call. `.length` isn't a field (it's a string's or array's length,
2733
+ * which `json_extract` can't read), so a path holding it fails.
2477
2734
  */
2478
2735
  parseFieldPath() {
2479
- if (!this.eat("dot")) {
2480
- return null;
2481
- }
2482
- if (!this.match("identifier")) {
2736
+ if (!this.eat("dot") || !this.match("identifier")) {
2483
2737
  return null;
2484
2738
  }
2485
2739
  const parts = [this.advance().value];
2486
- while (this.match("dot") && this.tokens[this.pos + 1]?.type === "identifier") {
2487
- if (this.tokens[this.pos + 2]?.type === "lparen") {
2740
+ while (this.match("dot") && this.match("identifier", void 0, 1)) {
2741
+ if (this.match("lparen", void 0, 2)) {
2488
2742
  break;
2489
2743
  }
2490
2744
  this.advance();
2491
2745
  parts.push(this.advance().value);
2492
2746
  }
2493
- if (parts.length === 1) {
2494
- return parts[0];
2495
- }
2496
- const root = parts[0];
2497
- const jsonPath = "$." + parts.slice(1).join(".");
2498
- return `json_extract(${root}, '${jsonPath}')`;
2499
- }
2500
- /**
2501
- * Parse a comparison: `field OP value`.
2502
- * The field has already been parsed; we need the operator and right-hand value.
2503
- */
2504
- parseComparison(field) {
2505
- const opToken = this.advance();
2506
- const jsOp = opToken.value;
2507
- const value = this.parseValue();
2508
- if (value === PARSE_FAILED) {
2509
- return null;
2510
- }
2511
- if (value === null || value === void 0) {
2512
- if (jsOp === "===" || jsOp === "==") {
2513
- return { kind: "nullCheck", field, isNull: true };
2514
- }
2515
- if (jsOp === "!==" || jsOp === "!=") {
2516
- return { kind: "nullCheck", field, isNull: false };
2517
- }
2747
+ if (parts.slice(1).includes("length")) {
2518
2748
  return null;
2519
2749
  }
2520
- const sqlOp = JS_TO_SQL_OP[jsOp];
2521
- if (!sqlOp) {
2522
- return null;
2750
+ const column = parts[0];
2751
+ if (parts.length === 1) {
2752
+ return { sql: column, column, nested: false };
2523
2753
  }
2524
- return { kind: "comparison", field, operator: sqlOp, value };
2754
+ const path = `$.${parts.slice(1).join(".")}`;
2755
+ return {
2756
+ sql: `json_extract(${column}, '${path}')`,
2757
+ column,
2758
+ nested: true,
2759
+ path
2760
+ };
2525
2761
  }
2526
- /**
2527
- * Parse `o.field.includes('text')` → LIKE expression.
2528
- * The field name has already been parsed.
2529
- */
2762
+ /** `o.field.includes(needle)`, the field already read. */
2530
2763
  parseFieldIncludes(field) {
2531
2764
  this.advance();
2532
2765
  this.advance();
2533
2766
  if (!this.eat("lparen")) {
2534
2767
  return null;
2535
2768
  }
2536
- const value = this.parseValue();
2537
- if (value === PARSE_FAILED || typeof value !== "string") {
2769
+ const needle = this.parseValue();
2770
+ if (typeof needle !== "string" && typeof needle !== "number" && typeof needle !== "boolean") {
2538
2771
  return null;
2539
2772
  }
2540
2773
  if (!this.eat("rparen")) {
2541
2774
  return null;
2542
2775
  }
2543
- const escaped = value.replace(/%/g, "\\%").replace(/_/g, "\\_");
2544
- return { kind: "like", field, pattern: `%${escaped}%` };
2776
+ return { kind: "includes", field, needle };
2545
2777
  }
2546
2778
  /**
2547
- * Parse `['a', 'b', 'c'].includes(o.field)` → IN expression.
2548
- * The opening bracket has been peeked but not consumed.
2779
+ * `[...].includes(o.field)` or `$.path.includes(o.field)`, when that's what
2780
+ * follows; null (position restored) otherwise.
2549
2781
  */
2550
- parseArrayIncludes() {
2551
- this.advance();
2552
- const values = [];
2553
- while (!this.match("rbracket")) {
2554
- if (values.length > 0) {
2555
- if (!this.eat("comma")) {
2782
+ tryParseMembership() {
2783
+ const start = this.pos;
2784
+ let values = null;
2785
+ if (this.eat("lbracket")) {
2786
+ const items = [];
2787
+ while (!this.match("rbracket")) {
2788
+ if (items.length > 0 && !this.eat("comma")) {
2789
+ this.pos = start;
2790
+ return null;
2791
+ }
2792
+ const item = this.parseValue();
2793
+ if (item === PARSE_FAILED) {
2794
+ this.pos = start;
2556
2795
  return null;
2557
2796
  }
2797
+ items.push(item);
2558
2798
  }
2559
- const val = this.parseValue();
2560
- if (val === PARSE_FAILED) {
2561
- return null;
2799
+ this.advance();
2800
+ values = items;
2801
+ } else if (this.bindingsName && this.match("identifier", this.bindingsName)) {
2802
+ const bound = this.tryResolveBindingsValue();
2803
+ if (Array.isArray(bound)) {
2804
+ values = bound;
2562
2805
  }
2563
- values.push(val);
2564
2806
  }
2565
- this.advance();
2566
- if (!this.eat("dot")) {
2567
- return null;
2568
- }
2569
- if (!this.match("identifier", "includes")) {
2807
+ if (!values || !this.lookAheadForIncludes() || !this.match("identifier", this.paramName, 3)) {
2808
+ this.pos = start;
2570
2809
  return null;
2571
2810
  }
2572
2811
  this.advance();
2573
- if (!this.eat("lparen")) {
2574
- return null;
2575
- }
2576
- if (!this.match("identifier", this.paramName)) {
2577
- return null;
2578
- }
2579
2812
  this.advance();
2580
- const field = this.parseFieldPath();
2581
- if (!field) {
2582
- return null;
2583
- }
2584
- if (!this.eat("rparen")) {
2585
- return null;
2586
- }
2587
- return { kind: "in", field, values };
2588
- }
2589
- /**
2590
- * Parse an expression that starts with an identifier that is NOT the
2591
- * parameter name. This could be:
2592
- * - A keyword literal: `true`, `false`, `null`, `undefined`
2593
- * - A closure variable used in a comparison (handled by backtracking)
2594
- */
2595
- parseNonParamExpression() {
2596
- const ident = this.peek().value;
2597
- if (ident === "true" || ident === "false") {
2598
- return null;
2599
- }
2600
- return null;
2601
- }
2602
- /**
2603
- * Parse `$.ids.includes(o.field)` → IN expression with resolved values.
2604
- * The bindings identifier has been peeked but not consumed.
2605
- *
2606
- * Falls back (returns null) if the resolved bindings value isn't an array,
2607
- * or if the path doesn't exist on the bindings object.
2608
- */
2609
- parseBindingsArrayIncludes() {
2610
- const bound = this.tryResolveBindingsValue();
2611
- if (bound === PARSE_FAILED) {
2612
- return null;
2613
- }
2614
- if (!Array.isArray(bound)) {
2615
- return null;
2616
- }
2617
- if (!this.eat("dot")) {
2618
- return null;
2619
- }
2620
- if (!this.match("identifier", "includes")) {
2621
- return null;
2622
- }
2623
2813
  this.advance();
2624
- if (!this.eat("lparen")) {
2625
- return null;
2626
- }
2627
- if (!this.match("identifier", this.paramName)) {
2628
- return null;
2629
- }
2630
2814
  this.advance();
2631
2815
  const field = this.parseFieldPath();
2632
- if (!field) {
2633
- return null;
2634
- }
2635
- if (!this.eat("rparen")) {
2816
+ if (!field || !this.eat("rparen")) {
2636
2817
  return null;
2637
2818
  }
2638
- return { kind: "in", field, values: bound };
2819
+ return { kind: "in", field, values };
2639
2820
  }
2640
2821
  /**
2641
- * If the current token is the bindings parameter name, walk a dotted path
2642
- * (`$.foo.bar`) and resolve the value from the bindings object. Returns
2643
- * the resolved value or `PARSE_FAILED`.
2644
- *
2645
- * Stops walking before a method call (e.g. doesn't consume `.includes` in
2646
- * `$.ids.includes(...)`) so the caller can dispatch on what follows.
2647
- *
2648
- * Restores `pos` on failure so callers can fall through cleanly.
2822
+ * Walk a dotted bindings path (`$.foo.bar`) and resolve it from the
2823
+ * bindings object. Stops before a method call (`.includes(`), so the caller
2824
+ * can dispatch on what follows. Restores `pos` and returns `PARSE_FAILED`
2825
+ * when the path doesn't resolve: a missing key or `undefined` value falls
2826
+ * back rather than being compared as NULL.
2649
2827
  */
2650
2828
  tryResolveBindingsValue() {
2651
- if (!this.bindingsName) {
2652
- return PARSE_FAILED;
2653
- }
2654
- if (!this.match("identifier", this.bindingsName)) {
2655
- return PARSE_FAILED;
2656
- }
2657
- if (this.bindingsValue == null) {
2829
+ if (!this.bindingsName || !this.match("identifier", this.bindingsName) || this.bindingsValue == null) {
2658
2830
  return PARSE_FAILED;
2659
2831
  }
2660
2832
  const startPos = this.pos;
2661
2833
  this.advance();
2662
2834
  const path = [];
2663
- while (this.match("dot") && this.tokens[this.pos + 1]?.type === "identifier") {
2664
- if (this.tokens[this.pos + 2]?.type === "lparen") {
2835
+ while (this.match("dot") && this.match("identifier", void 0, 1)) {
2836
+ if (this.match("lparen", void 0, 2)) {
2665
2837
  break;
2666
2838
  }
2667
2839
  this.advance();
2668
2840
  path.push(this.advance().value);
2669
2841
  }
2670
- if (path.length === 0) {
2671
- this.pos = startPos;
2672
- return PARSE_FAILED;
2673
- }
2674
2842
  let value = this.bindingsValue;
2675
2843
  for (const key of path) {
2676
2844
  if (value == null || typeof value !== "object") {
2677
- this.pos = startPos;
2678
- return PARSE_FAILED;
2845
+ value = void 0;
2846
+ break;
2679
2847
  }
2680
2848
  value = value[key];
2681
2849
  }
2682
- if (value === void 0) {
2850
+ if (path.length === 0 || value === void 0) {
2683
2851
  this.pos = startPos;
2684
2852
  return PARSE_FAILED;
2685
2853
  }
2686
2854
  return value;
2687
2855
  }
2688
- /**
2689
- * Parse a literal value or closure variable reference.
2690
- *
2691
- * Returns the parsed value, or PARSE_FAILED if parsing fails.
2692
- * Returns `null` or `undefined` for those keyword literals.
2693
- */
2856
+ // --- Values: constant expressions, evaluated here ---
2857
+ /** A value: a literal, binding or helper call, or arithmetic over numbers. */
2694
2858
  parseValue() {
2859
+ let left = this.parseTerm();
2860
+ while (this.match("operator", "+") || this.match("operator", "-")) {
2861
+ const op = this.advance().value;
2862
+ const right = this.parseTerm();
2863
+ if (typeof left !== "number" || typeof right !== "number") {
2864
+ return PARSE_FAILED;
2865
+ }
2866
+ left = op === "+" ? left + right : left - right;
2867
+ }
2868
+ return left;
2869
+ }
2870
+ parseTerm() {
2871
+ let left = this.parseUnary();
2872
+ while (this.match("operator", "*") || this.match("operator", "/") || this.match("operator", "%")) {
2873
+ const op = this.advance().value;
2874
+ const right = this.parseUnary();
2875
+ if (typeof left !== "number" || typeof right !== "number") {
2876
+ return PARSE_FAILED;
2877
+ }
2878
+ left = op === "*" ? left * right : op === "/" ? left / right : left % right;
2879
+ }
2880
+ return left;
2881
+ }
2882
+ parseUnary() {
2883
+ if (this.match("operator", "-") || this.match("operator", "+")) {
2884
+ const op = this.advance().value;
2885
+ const operand = this.parseUnary();
2886
+ if (typeof operand !== "number") {
2887
+ return PARSE_FAILED;
2888
+ }
2889
+ return op === "-" ? -operand : operand;
2890
+ }
2891
+ return this.parseAtom();
2892
+ }
2893
+ parseAtom() {
2695
2894
  const t = this.peek();
2696
2895
  if (!t) {
2697
2896
  return PARSE_FAILED;
@@ -2702,85 +2901,82 @@ var init_predicate = __esm({
2702
2901
  }
2703
2902
  if (t.type === "number") {
2704
2903
  this.advance();
2705
- return Number(t.value);
2904
+ return t.number;
2905
+ }
2906
+ if (t.type === "lparen") {
2907
+ this.advance();
2908
+ const inner = this.parseValue();
2909
+ return this.eat("rparen") ? inner : PARSE_FAILED;
2910
+ }
2911
+ if (t.type !== "identifier") {
2912
+ return PARSE_FAILED;
2706
2913
  }
2707
- if (t.type === "identifier") {
2708
- if (t.value === "true") {
2914
+ switch (t.value) {
2915
+ case "true":
2709
2916
  this.advance();
2710
2917
  return true;
2711
- }
2712
- if (t.value === "false") {
2918
+ case "false":
2713
2919
  this.advance();
2714
2920
  return false;
2715
- }
2716
- if (t.value === "null") {
2921
+ case "null":
2717
2922
  this.advance();
2718
2923
  return null;
2719
- }
2720
- if (t.value === "undefined") {
2924
+ case "undefined":
2721
2925
  this.advance();
2722
2926
  return void 0;
2723
- }
2724
- if (this.bindingsName && t.value === this.bindingsName) {
2725
- const bound = this.tryResolveBindingsValue();
2726
- if (bound !== PARSE_FAILED) {
2727
- return bound;
2927
+ case "void":
2928
+ if (this.match("number", void 0, 1)) {
2929
+ this.advance();
2930
+ this.advance();
2931
+ return void 0;
2728
2932
  }
2729
- }
2730
- return this.resolveClosureVariable();
2933
+ return PARSE_FAILED;
2731
2934
  }
2732
- if (t.type === "operator" && t.value === "-") {
2733
- this.advance();
2734
- const next = this.peek();
2735
- if (next?.type === "number") {
2935
+ if (this.bindingsName && t.value === this.bindingsName) {
2936
+ return this.tryResolveBindingsValue();
2937
+ }
2938
+ if (t.value === "Date" && this.match("dot", void 0, 1) && this.match("identifier", "now", 2) && this.match("lparen", void 0, 3) && this.match("rparen", void 0, 4)) {
2939
+ this.pos += 5;
2940
+ return Date.now();
2941
+ }
2942
+ if (t.value === "db" && this.match("dot", void 0, 1) && this.match("lparen", void 0, 3)) {
2943
+ const name = this.peek(2).value;
2944
+ if (Object.hasOwn(timeHelpers, name)) {
2945
+ this.pos += 4;
2946
+ const args = [];
2947
+ while (!this.match("rparen")) {
2948
+ if (args.length > 0 && !this.eat("comma")) {
2949
+ return PARSE_FAILED;
2950
+ }
2951
+ const arg = this.parseValue();
2952
+ if (typeof arg !== "number") {
2953
+ return PARSE_FAILED;
2954
+ }
2955
+ args.push(arg);
2956
+ }
2736
2957
  this.advance();
2737
- return -Number(next.value);
2958
+ const helper = timeHelpers[name];
2959
+ return helper(...args);
2738
2960
  }
2739
- return PARSE_FAILED;
2740
- }
2741
- return PARSE_FAILED;
2742
- }
2743
- /**
2744
- * Attempt to resolve a closure variable's value.
2745
- *
2746
- * This handles the common pattern:
2747
- * ```ts
2748
- * const userId = auth.userId;
2749
- * orders.filter(o => o.requestedBy === userId)
2750
- * ```
2751
- *
2752
- * Closure variable resolution is fundamentally limited in JavaScript —
2753
- * we can't access another function's closure scope from outside without
2754
- * `eval`. The `===` operator can't be overridden via Proxy or
2755
- * Symbol.toPrimitive, so we can't intercept comparisons.
2756
- *
2757
- * For now, this falls back to JS execution. The predicate still works
2758
- * correctly — it just scans all rows instead of generating SQL.
2759
- * This is the most common reason for JS fallback in practice, since
2760
- * almost every real-world filter references a variable like `userId`.
2761
- *
2762
- * A future improvement could accept an explicit `vars` argument:
2763
- * ```ts
2764
- * orders.filter(o => o.requestedBy === $userId, { $userId: auth.userId })
2765
- * ```
2766
- */
2767
- resolveClosureVariable() {
2768
- this.advance();
2769
- while (this.match("dot") && this.tokens[this.pos + 1]?.type === "identifier") {
2770
- this.advance();
2771
- this.advance();
2772
2961
  }
2773
2962
  return PARSE_FAILED;
2774
2963
  }
2775
- /**
2776
- * Look ahead to check if the next tokens form `.includes(`.
2777
- * Used to disambiguate `o.field.includes(...)` from `o.field.nested`.
2778
- */
2964
+ /** Whether the next tokens are `.includes(`. */
2779
2965
  lookAheadForIncludes() {
2780
- return this.tokens[this.pos]?.type === "dot" && this.tokens[this.pos + 1]?.type === "identifier" && this.tokens[this.pos + 1]?.value === "includes" && this.tokens[this.pos + 2]?.type === "lparen";
2966
+ return this.match("dot") && this.match("identifier", "includes", 1) && this.match("lparen", void 0, 2);
2781
2967
  }
2782
2968
  };
2783
- JS_TO_SQL_OP = {
2969
+ NEGATED = {
2970
+ "===": "!==",
2971
+ "!==": "===",
2972
+ "==": "!=",
2973
+ "!=": "==",
2974
+ "<": ">=",
2975
+ ">=": "<",
2976
+ ">": "<=",
2977
+ "<=": ">"
2978
+ };
2979
+ SQL_OP = {
2784
2980
  "===": "=",
2785
2981
  "==": "=",
2786
2982
  "!==": "!=",
@@ -2790,7 +2986,16 @@ var init_predicate = __esm({
2790
2986
  "<=": "<=",
2791
2987
  ">=": ">="
2792
2988
  };
2793
- PARSE_FAILED = /* @__PURE__ */ Symbol("PARSE_FAILED");
2989
+ VALUE_KIND = {
2990
+ text: "string",
2991
+ user: "string",
2992
+ number: "number",
2993
+ boolean: "boolean"
2994
+ };
2995
+ JSON_TYPES = {
2996
+ string: "('text')",
2997
+ number: "('integer', 'real')"
2998
+ };
2794
2999
  }
2795
3000
  });
2796
3001
 
@@ -2893,24 +3098,6 @@ function aggregateValues(fn, values) {
2893
3098
  return present.length === 0 ? null : present.reduce((m, v) => v > m ? v : m);
2894
3099
  }
2895
3100
  }
2896
- function compareForOrder(a, b) {
2897
- if (a == null && b == null) {
2898
- return 0;
2899
- }
2900
- if (a == null) {
2901
- return -1;
2902
- }
2903
- if (b == null) {
2904
- return 1;
2905
- }
2906
- if (a < b) {
2907
- return -1;
2908
- }
2909
- if (a > b) {
2910
- return 1;
2911
- }
2912
- return 0;
2913
- }
2914
3101
  function computeAggregateInJs(rows, spec) {
2915
3102
  const groups = /* @__PURE__ */ new Map();
2916
3103
  for (const row of rows) {
@@ -2940,7 +3127,7 @@ function computeAggregateInJs(rows, spec) {
2940
3127
  });
2941
3128
  if (spec.orderBy) {
2942
3129
  const key = spec.orderBy;
2943
- out.sort((a, b) => compareForOrder(a[key], b[key]));
3130
+ out.sort((a, b) => compareAsSqlite(a[key], b[key]));
2944
3131
  if (spec.desc) {
2945
3132
  out.reverse();
2946
3133
  }
@@ -3025,6 +3212,8 @@ var init_query = __esm({
3025
3212
  _rawWhereParams;
3026
3213
  /** @internal See AggregateState — set by aggregate terminals. */
3027
3214
  _aggregate;
3215
+ /** @internal The columns `select()` named; every column when unset. */
3216
+ _columns;
3028
3217
  /** @internal Post-process transform applied after row deserialization. */
3029
3218
  _postProcess;
3030
3219
  constructor(config, options) {
@@ -3038,6 +3227,7 @@ var init_query = __esm({
3038
3227
  this._rawWhere = options?.rawWhere;
3039
3228
  this._rawWhereParams = options?.rawWhereParams;
3040
3229
  this._aggregate = options?.aggregate;
3230
+ this._columns = options?.columns;
3041
3231
  }
3042
3232
  _clone(overrides) {
3043
3233
  return new _Query(this._config, {
@@ -3049,7 +3239,8 @@ var init_query = __esm({
3049
3239
  postProcess: overrides.postProcess,
3050
3240
  rawWhere: this._rawWhere,
3051
3241
  rawWhereParams: this._rawWhereParams,
3052
- aggregate: overrides.aggregate ?? this._aggregate
3242
+ aggregate: overrides.aggregate ?? this._aggregate,
3243
+ columns: overrides.columns ?? this._columns
3053
3244
  });
3054
3245
  }
3055
3246
  filter(predicate, bindings) {
@@ -3069,6 +3260,21 @@ var init_query = __esm({
3069
3260
  skip(n) {
3070
3261
  return this._clone({ offset: n });
3071
3262
  }
3263
+ /**
3264
+ * Return only these columns: `SELECT id, name` rather than every column of
3265
+ * a wide row. Chain it before a terminal (`first()`, `take()`); filters and
3266
+ * sorts still see the whole row.
3267
+ *
3268
+ * ```ts
3269
+ * const names = await Companies
3270
+ * .filter((c, $) => c.firmId === $.firmId, { firmId })
3271
+ * .select('id', 'name');
3272
+ * // Array<{ id: string; name: string }>
3273
+ * ```
3274
+ */
3275
+ select(...columns) {
3276
+ return this._clone({ columns });
3277
+ }
3072
3278
  // -------------------------------------------------------------------------
3073
3279
  // Terminal methods
3074
3280
  // -------------------------------------------------------------------------
@@ -3156,8 +3362,8 @@ var init_query = __esm({
3156
3362
  if (compiled.allSql && compiled.sqlWhere) {
3157
3363
  const query = buildExists(
3158
3364
  this._config.tableName,
3159
- `NOT (${compiled.sqlWhere})`,
3160
- void 0,
3365
+ `NOT coalesce(${compiled.sqlWhere}, 0)`,
3366
+ compiled.params,
3161
3367
  true
3162
3368
  );
3163
3369
  const results = await this._config.executeBatch([query]);
@@ -3199,116 +3405,152 @@ var init_query = __esm({
3199
3405
  // Batch compilation — used by db.batch() to extract SQL without executing
3200
3406
  // -------------------------------------------------------------------------
3201
3407
  /**
3202
- * @internal Compile this query into a SqlQuery for batch execution.
3203
- *
3204
- * Returns the compiled SQL query (if all predicates compile to SQL),
3205
- * or null (if JS fallback is needed). In the fallback case, a bare
3206
- * `SELECT *` is returned as `fallbackQuery` so the batch can fetch
3207
- * all rows and this query can filter them in JS post-fetch.
3408
+ * @internal Compile this query into the statement to run, plus whatever
3409
+ * JS has to do with its rows (see the module header's three cases). Used by
3410
+ * db.batch() and by a standalone await alike.
3208
3411
  */
3209
3412
  _compile() {
3413
+ const table = this._config.tableName;
3210
3414
  if (this._rawWhere) {
3211
- const query = buildSelect(this._config.tableName, {
3212
- where: this._rawWhere,
3213
- whereParams: this._rawWhereParams,
3214
- orderBy: void 0,
3215
- limit: this._limit,
3216
- offset: this._offset
3217
- });
3218
3415
  return {
3219
3416
  type: "query",
3220
- query,
3221
- fallbackQuery: null,
3417
+ query: buildSelect(table, {
3418
+ columns: this._columns,
3419
+ where: this._rawWhere,
3420
+ whereParams: this._rawWhereParams,
3421
+ limit: this._limit,
3422
+ offset: this._offset
3423
+ }),
3222
3424
  config: this._config,
3223
3425
  postProcess: this._postProcess
3224
3426
  };
3225
3427
  }
3226
3428
  const compiled = this._compilePredicates();
3227
- const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
3228
- if (compiled.allSql) {
3229
- const agg = this._compileAggregateSql(compiled.sqlWhere);
3230
- if (agg) {
3231
- return {
3232
- type: "query",
3233
- query: agg.query,
3234
- fallbackQuery: null,
3235
- config: this._config,
3236
- processRaw: agg.processRaw
3237
- };
3238
- }
3239
- const query = buildSelect(this._config.tableName, {
3240
- where: compiled.sqlWhere || void 0,
3241
- orderBy: sortField ?? void 0,
3242
- desc: this._reversed,
3243
- limit: this._limit,
3244
- offset: this._offset
3245
- });
3429
+ if (!compiled.allSql) {
3430
+ const reasons = compiled.compiled.flatMap(
3431
+ (c) => c.type === "js" && c.reason ? [c.reason] : []
3432
+ );
3433
+ return {
3434
+ type: "query",
3435
+ query: buildSelect(table, {
3436
+ orderBy: !this._sortAccessor && this._reversed ? "rowid" : void 0
3437
+ }),
3438
+ config: this._config,
3439
+ inJs: {
3440
+ why: `filter could not be compiled to SQL${reasons.length ? ` (${reasons.join("; ")})` : ""}`,
3441
+ predicates: this._predicates,
3442
+ sortAccessor: this._sortAccessor,
3443
+ reversed: this._reversed,
3444
+ limit: this._limit,
3445
+ offset: this._offset,
3446
+ columns: this._columns
3447
+ },
3448
+ postProcess: this._postProcess
3449
+ };
3450
+ }
3451
+ const agg = this._compileAggregateSql(compiled.sqlWhere, compiled.params);
3452
+ if (agg) {
3246
3453
  return {
3247
3454
  type: "query",
3248
- query,
3249
- fallbackQuery: null,
3455
+ query: agg.query,
3250
3456
  config: this._config,
3457
+ processRaw: agg.processRaw
3458
+ };
3459
+ }
3460
+ const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : null;
3461
+ if (this._sortAccessor && sortField == null) {
3462
+ return {
3463
+ type: "query",
3464
+ query: buildSelect(table, {
3465
+ where: compiled.sqlWhere || void 0,
3466
+ whereParams: compiled.params
3467
+ }),
3468
+ config: this._config,
3469
+ inJs: {
3470
+ why: "sortBy is computed, not a column, so it sorts in JS",
3471
+ sortAccessor: this._sortAccessor,
3472
+ reversed: this._reversed,
3473
+ limit: this._limit,
3474
+ offset: this._offset,
3475
+ columns: this._columns
3476
+ },
3251
3477
  postProcess: this._postProcess
3252
3478
  };
3253
3479
  }
3254
- const fallbackQuery = buildSelect(this._config.tableName);
3255
3480
  return {
3256
3481
  type: "query",
3257
- query: null,
3258
- fallbackQuery,
3482
+ query: buildSelect(table, {
3483
+ columns: this._columns,
3484
+ where: compiled.sqlWhere || void 0,
3485
+ whereParams: compiled.params,
3486
+ orderBy: sortField ?? (this._reversed ? "rowid" : void 0),
3487
+ desc: this._reversed,
3488
+ limit: this._limit,
3489
+ offset: this._offset
3490
+ }),
3259
3491
  config: this._config,
3260
- predicates: this._predicates,
3261
- sortAccessor: this._sortAccessor,
3262
- reversed: this._reversed,
3263
- limit: this._limit,
3264
- offset: this._offset,
3265
3492
  postProcess: this._postProcess
3266
3493
  };
3267
3494
  }
3268
3495
  /**
3269
3496
  * @internal Process raw SQL results into typed rows. Used by db.batch()
3270
- * after executing the compiled query.
3497
+ * after executing the compiled query, and by a standalone await.
3271
3498
  *
3272
- * For SQL-compiled queries: just deserialize the rows.
3273
- * For JS-fallback queries: filter, sort, and slice in JS.
3499
+ * Deserializes the rows, then does in JS whatever the SQL couldn't
3500
+ * (`inJs`): the filter, the sort, the page, the projection.
3274
3501
  */
3275
3502
  static _processResults(result, compiled) {
3276
3503
  if (compiled.processRaw) {
3277
3504
  return compiled.processRaw(result);
3278
3505
  }
3279
- const rows = result.rows.map(
3280
- (row) => deserializeRow(
3281
- row,
3282
- compiled.config.columns
3283
- )
3506
+ const columns = compiled.config.columns;
3507
+ let rows = result.rows.map(
3508
+ (row) => deserializeRow(row, columns)
3284
3509
  );
3285
- if (compiled.query) {
3286
- return compiled.postProcess ? compiled.postProcess(rows) : rows;
3287
- }
3288
- let filtered = compiled.predicates ? rows.filter(
3289
- (row) => compiled.predicates.every((p) => p.fn(row, p.bindings))
3290
- ) : rows;
3291
- if (compiled.sortAccessor) {
3292
- const accessor = compiled.sortAccessor;
3293
- const reversed = compiled.reversed ?? false;
3294
- filtered.sort((a, b) => {
3295
- const aVal = accessor(a);
3296
- const bVal = accessor(b);
3297
- if (aVal < bVal) {
3298
- return reversed ? 1 : -1;
3299
- }
3300
- if (aVal > bVal) {
3301
- return reversed ? -1 : 1;
3302
- }
3303
- return 0;
3304
- });
3510
+ const inJs = compiled.inJs;
3511
+ if (inJs) {
3512
+ if (inJs.predicates?.length) {
3513
+ rows = rows.filter(
3514
+ (row) => inJs.predicates.every((p) => p.fn(row, p.bindings))
3515
+ );
3516
+ }
3517
+ if (inJs.sortAccessor) {
3518
+ const accessor = inJs.sortAccessor;
3519
+ const direction = inJs.reversed ? -1 : 1;
3520
+ rows.sort(
3521
+ (a, b) => direction * compareAsSqlite(accessor(a), accessor(b))
3522
+ );
3523
+ } else if (inJs.reversed) {
3524
+ rows.reverse();
3525
+ }
3526
+ if (inJs.offset != null || inJs.limit != null) {
3527
+ const start = inJs.offset ?? 0;
3528
+ const end = inJs.limit != null ? start + inJs.limit : void 0;
3529
+ rows = rows.slice(start, end);
3530
+ }
3531
+ if (inJs.columns?.length) {
3532
+ const keep = inJs.columns;
3533
+ rows = rows.map(
3534
+ (row) => Object.fromEntries(
3535
+ keep.map((column) => [
3536
+ column,
3537
+ row[column]
3538
+ ])
3539
+ )
3540
+ );
3541
+ }
3305
3542
  }
3306
- if (compiled.offset != null || compiled.limit != null) {
3307
- const start = compiled.offset ?? 0;
3308
- const end = compiled.limit != null ? start + compiled.limit : void 0;
3309
- filtered = filtered.slice(start, end);
3543
+ return compiled.postProcess ? compiled.postProcess(rows) : rows;
3544
+ }
3545
+ /** @internal Say that part of a query ran in JS, and why. */
3546
+ static _warnInJs(compiled, rowsRead) {
3547
+ if (!compiled.inJs) {
3548
+ return;
3310
3549
  }
3311
- return compiled.postProcess ? compiled.postProcess(filtered) : filtered;
3550
+ const read = rowsRead == null ? "" : ` (${rowsRead} rows read)`;
3551
+ console.warn(
3552
+ `[mindstudio] Query on '${compiled.config.tableName}': ${compiled.inJs.why} \u2014 processing in JS${read}`
3553
+ );
3312
3554
  }
3313
3555
  // -------------------------------------------------------------------------
3314
3556
  // PromiseLike
@@ -3318,18 +3560,10 @@ var init_query = __esm({
3318
3560
  }
3319
3561
  async _run() {
3320
3562
  await this._config.ready?.();
3321
- if (this._aggregate && !this._rawWhere) {
3322
- const compiled = this._compilePredicates();
3323
- if (compiled.allSql) {
3324
- const agg = this._compileAggregateSql(compiled.sqlWhere);
3325
- if (agg) {
3326
- const results = await this._config.executeBatch([agg.query]);
3327
- return agg.processRaw(results[0]);
3328
- }
3329
- }
3330
- }
3331
- const rows = await this._execute();
3332
- return this._postProcess ? this._postProcess(rows) : rows;
3563
+ const compiled = this._compile();
3564
+ const results = await this._config.executeBatch([compiled.query]);
3565
+ _Query._warnInJs(compiled, results[0]?.rows.length);
3566
+ return _Query._processResults(results[0], compiled);
3333
3567
  }
3334
3568
  /**
3335
3569
  * @internal Compile the pending aggregate into a single SQL statement +
@@ -3340,7 +3574,7 @@ var init_query = __esm({
3340
3574
  * when a scalar accessor doesn't extract to a column (computed accessors
3341
3575
  * stay correct via JS, with a warning to prompt optimization).
3342
3576
  */
3343
- _compileAggregateSql(sqlWhere) {
3577
+ _compileAggregateSql(sqlWhere, whereParams) {
3344
3578
  const agg = this._aggregate;
3345
3579
  if (!agg || this._rawWhere || this._limit != null || this._offset != null) {
3346
3580
  return null;
@@ -3361,7 +3595,8 @@ var init_query = __esm({
3361
3595
  this._config.tableName,
3362
3596
  agg.fn,
3363
3597
  column,
3364
- where
3598
+ where,
3599
+ whereParams
3365
3600
  );
3366
3601
  const processRaw2 = (result) => {
3367
3602
  const value = result.rows[0]?.value;
@@ -3377,6 +3612,7 @@ var init_query = __esm({
3377
3612
  by: spec.by,
3378
3613
  select: spec.select,
3379
3614
  where,
3615
+ whereParams,
3380
3616
  orderBy: spec.orderBy,
3381
3617
  desc: spec.desc,
3382
3618
  limit: spec.limit
@@ -3397,89 +3633,22 @@ var init_query = __esm({
3397
3633
  // -------------------------------------------------------------------------
3398
3634
  // Execution internals
3399
3635
  // -------------------------------------------------------------------------
3400
- async _execute() {
3401
- if (this._rawWhere) {
3402
- const query = buildSelect(this._config.tableName, {
3403
- where: this._rawWhere,
3404
- whereParams: this._rawWhereParams,
3405
- limit: this._limit,
3406
- offset: this._offset
3407
- });
3408
- const results = await this._config.executeBatch([query]);
3409
- return results[0].rows.map(
3410
- (row) => deserializeRow(
3411
- row,
3412
- this._config.columns
3413
- )
3414
- );
3415
- }
3416
- const compiled = this._compilePredicates();
3417
- if (compiled.allSql) {
3418
- const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
3419
- const query = buildSelect(this._config.tableName, {
3420
- where: compiled.sqlWhere || void 0,
3421
- orderBy: sortField ?? void 0,
3422
- desc: this._reversed,
3423
- limit: this._limit,
3424
- offset: this._offset
3425
- });
3426
- const results = await this._config.executeBatch([query]);
3427
- return results[0].rows.map(
3428
- (row) => deserializeRow(
3429
- row,
3430
- this._config.columns
3431
- )
3432
- );
3433
- }
3434
- let rows = await this._fetchAndFilterInJs(compiled);
3435
- if (this._sortAccessor) {
3436
- const accessor = this._sortAccessor;
3437
- rows.sort((a, b) => {
3438
- const aVal = accessor(a);
3439
- const bVal = accessor(b);
3440
- if (aVal < bVal) {
3441
- return this._reversed ? 1 : -1;
3442
- }
3443
- if (aVal > bVal) {
3444
- return this._reversed ? -1 : 1;
3445
- }
3446
- return 0;
3447
- });
3448
- }
3449
- if (this._offset != null || this._limit != null) {
3450
- const start = this._offset ?? 0;
3451
- const end = this._limit != null ? start + this._limit : void 0;
3452
- rows = rows.slice(start, end);
3453
- }
3454
- return rows;
3455
- }
3456
3636
  _compilePredicates() {
3457
3637
  if (this._predicates.length === 0) {
3458
- return { allSql: true, sqlWhere: "", compiled: [] };
3638
+ return { allSql: true, sqlWhere: "", params: [], compiled: [] };
3459
3639
  }
3640
+ const columns = this._config.columns;
3460
3641
  const compiled = this._predicates.map(
3461
- (p) => compilePredicate(p.fn, p.bindings, this._config.columns)
3462
- );
3463
- const allSql = compiled.every((c) => c.type === "sql");
3464
- let sqlWhere = "";
3465
- if (allSql) {
3466
- sqlWhere = compiled.map((c) => c.where).join(" AND ");
3467
- }
3468
- return { allSql, sqlWhere, compiled };
3469
- }
3470
- async _fetchAndFilterInJs(compiled) {
3471
- const allRows = await this._fetchAllRows();
3472
- const jsFallbacks = compiled.compiled.filter((c) => c.type === "js");
3473
- if (jsFallbacks.length > 0) {
3474
- const reasons = jsFallbacks.map((c) => c.type === "js" ? c.reason : void 0).filter(Boolean);
3475
- const reasonSuffix = reasons.length > 0 ? ` (${reasons.join("; ")})` : "";
3476
- console.warn(
3477
- `[mindstudio] Filter on '${this._config.tableName}' could not be compiled to SQL${reasonSuffix} \u2014 scanning ${allRows.length} rows in JS`
3478
- );
3479
- }
3480
- return allRows.filter(
3481
- (row) => this._predicates.every((p) => p.fn(row, p.bindings))
3642
+ (p) => compilePredicate(p.fn, p.bindings, columns)
3482
3643
  );
3644
+ const sql = compiled.flatMap((c) => c.type === "sql" ? [c] : []);
3645
+ const allSql = sql.length === compiled.length;
3646
+ return {
3647
+ allSql,
3648
+ sqlWhere: allSql ? sql.map((c) => c.where).join(" AND ") : "",
3649
+ params: allSql ? sql.flatMap((c) => c.params) : [],
3650
+ compiled
3651
+ };
3483
3652
  }
3484
3653
  async _fetchAllRows() {
3485
3654
  const query = buildSelect(this._config.tableName);
@@ -3498,38 +3667,20 @@ var init_mutation = __esm({
3498
3667
  "src/db/mutation.ts"() {
3499
3668
  "use strict";
3500
3669
  init_errors();
3501
- Mutation = class _Mutation {
3670
+ Mutation = class {
3502
3671
  /** @internal */
3503
3672
  _config;
3504
- /** @internal */
3673
+ /** @internal The statements, or null when the write can't be one batch. */
3505
3674
  _build;
3506
3675
  /** @internal */
3507
3676
  _processResult;
3508
- /** @internal Non-batchable executor for complex mutations (e.g. removeAll JS fallback). */
3509
- _executor;
3510
- constructor(config, build, processResult) {
3677
+ /** @internal What runs when `_build` returns null (e.g. removeAll's JS fallback). */
3678
+ _fallback;
3679
+ constructor(config, build, processResult, fallback) {
3511
3680
  this._config = config;
3512
3681
  this._build = build;
3513
3682
  this._processResult = processResult;
3514
- this._executor = void 0;
3515
- }
3516
- /**
3517
- * Create a non-batchable mutation that wraps an async executor.
3518
- * Used for operations that require multi-step execution (e.g. removeAll
3519
- * with a JS-fallback predicate: fetch all rows → filter → delete).
3520
- *
3521
- * Works fine when awaited standalone. Throws if passed to db.batch().
3522
- *
3523
- * @internal
3524
- */
3525
- static fromExecutor(config, executor) {
3526
- const m = new _Mutation(
3527
- config,
3528
- () => [],
3529
- () => void 0
3530
- );
3531
- Object.defineProperty(m, "_executor", { value: executor });
3532
- return m;
3683
+ this._fallback = fallback;
3533
3684
  }
3534
3685
  // -------------------------------------------------------------------------
3535
3686
  // PromiseLike — executes on await
@@ -3547,10 +3698,11 @@ var init_mutation = __esm({
3547
3698
  * @internal Compile this mutation into SQL for batch execution.
3548
3699
  * Returns the queries and a result processor.
3549
3700
  *
3550
- * Throws if this is a non-batchable mutation (created via fromExecutor).
3701
+ * Throws if it can't be expressed as SQL (its builder returned null).
3551
3702
  */
3552
3703
  _compile() {
3553
- if (this._executor) {
3704
+ const queries = this._build();
3705
+ if (!queries) {
3554
3706
  throw new MindStudioError(
3555
3707
  "This operation cannot be batched (e.g. removeAll with a JS-fallback predicate). Await it separately instead of passing to db.batch().",
3556
3708
  "not_batchable",
@@ -3559,7 +3711,7 @@ var init_mutation = __esm({
3559
3711
  }
3560
3712
  return {
3561
3713
  type: "mutation",
3562
- queries: this._build(),
3714
+ queries,
3563
3715
  config: this._config,
3564
3716
  processResult: this._processResult
3565
3717
  };
@@ -3576,10 +3728,11 @@ var init_mutation = __esm({
3576
3728
  // -------------------------------------------------------------------------
3577
3729
  async _execute() {
3578
3730
  await this._config.ready?.();
3579
- if (this._executor) {
3580
- return this._executor();
3731
+ const queries = this._build();
3732
+ if (!queries) {
3733
+ return this._fallback();
3581
3734
  }
3582
- const results = await this._config.executeBatch(this._build());
3735
+ const results = await this._config.executeBatch(queries);
3583
3736
  return this._processResult(results);
3584
3737
  }
3585
3738
  };
@@ -3694,6 +3847,10 @@ var init_table = __esm({
3694
3847
  sortBy(accessor) {
3695
3848
  return new Query(this._config).sortBy(accessor);
3696
3849
  }
3850
+ /** Every row, with only these columns. See Query.select(). */
3851
+ select(...columns) {
3852
+ return new Query(this._config).select(...columns);
3853
+ }
3697
3854
  push(data) {
3698
3855
  const isArray = Array.isArray(data);
3699
3856
  const items = (isArray ? data : [data]).map(
@@ -3763,43 +3920,48 @@ var init_table = __esm({
3763
3920
  })
3764
3921
  );
3765
3922
  }
3766
- removeAll(predicate, bindings) {
3767
- const sqlWhere = () => {
3768
- const compiled = compilePredicate(
3769
- predicate,
3770
- bindings,
3771
- this._config.columns
3772
- );
3773
- return compiled.type === "sql" ? compiled.where : null;
3774
- };
3775
- if (sqlWhere() !== null) {
3776
- return new Mutation(
3777
- this._config,
3778
- () => [buildDelete(this._config.tableName, sqlWhere())],
3779
- (results) => results[0].changes
3780
- );
3781
- }
3782
- return Mutation.fromExecutor(this._config, async () => {
3783
- console.warn(
3784
- `[mindstudio] removeAll predicate on ${this._config.tableName} could not be compiled to SQL \u2014 fetching all rows first`
3785
- );
3786
- const allQuery = buildSelect(this._config.tableName);
3787
- const allResults = await this._config.executeBatch([allQuery]);
3788
- const allRows = allResults[0].rows.map(
3789
- (r) => deserializeRow(r, this._config.columns)
3790
- );
3791
- const matching = allRows.filter((row) => predicate(row, bindings));
3792
- if (matching.length === 0) {
3793
- return 0;
3794
- }
3795
- const deleteQueries = matching.filter((row) => row.id).map(
3796
- (row) => buildDelete(this._config.tableName, `id = ?`, [row.id])
3797
- );
3798
- if (deleteQueries.length > 0) {
3799
- await this._config.executeBatch(deleteQueries);
3923
+ removeAll(predicate, bindings) {
3924
+ return new Mutation(
3925
+ this._config,
3926
+ () => {
3927
+ const compiled = compilePredicate(
3928
+ predicate,
3929
+ bindings,
3930
+ this._config.columns
3931
+ );
3932
+ return compiled.type === "sql" ? [
3933
+ buildDelete(
3934
+ this._config.tableName,
3935
+ compiled.where,
3936
+ compiled.params
3937
+ )
3938
+ ] : null;
3939
+ },
3940
+ (results) => results[0].changes,
3941
+ // Fallback: multi-step execution — not batchable
3942
+ async () => {
3943
+ console.warn(
3944
+ `[mindstudio] removeAll predicate on ${this._config.tableName} could not be compiled to SQL \u2014 fetching all rows first`
3945
+ );
3946
+ const allQuery = buildSelect(this._config.tableName);
3947
+ const allResults = await this._config.executeBatch([allQuery]);
3948
+ const columns = this._config.columns;
3949
+ const allRows = allResults[0].rows.map(
3950
+ (r) => deserializeRow(r, columns)
3951
+ );
3952
+ const matching = allRows.filter((row) => predicate(row, bindings));
3953
+ if (matching.length === 0) {
3954
+ return 0;
3955
+ }
3956
+ const deleteQueries = matching.filter((row) => row.id).map(
3957
+ (row) => buildDelete(this._config.tableName, `id = ?`, [row.id])
3958
+ );
3959
+ if (deleteQueries.length > 0) {
3960
+ await this._config.executeBatch(deleteQueries);
3961
+ }
3962
+ return matching.length;
3800
3963
  }
3801
- return matching.length;
3802
- });
3964
+ );
3803
3965
  }
3804
3966
  clear() {
3805
3967
  const query = buildDelete(this._config.tableName);
@@ -4013,12 +4175,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
4013
4175
  },
4014
4176
  // --- Time helpers ---
4015
4177
  // Pure JS, no platform dependency. All timestamps are unix ms.
4016
- now: () => Date.now(),
4017
- days: (n) => n * 864e5,
4018
- hours: (n) => n * 36e5,
4019
- minutes: (n) => n * 6e4,
4020
- ago: (ms) => Date.now() - ms,
4021
- fromNow: (ms) => Date.now() + ms,
4178
+ ...timeHelpers,
4022
4179
  // --- User references ---
4023
4180
  userRef: (id) => id.startsWith(USER_PREFIX) ? id.slice(USER_PREFIX.length) : id,
4024
4181
  // --- Raw SQL (read-only) ---
@@ -4058,8 +4215,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
4058
4215
  groups.set(dbId, []);
4059
4216
  }
4060
4217
  if (c.type === "query") {
4061
- const sqlQuery = c.query ?? c.fallbackQuery;
4062
- groups.get(dbId).push({ opIndex: i, sqlQueries: [sqlQuery] });
4218
+ groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
4063
4219
  } else if (c.type === "raw") {
4064
4220
  groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
4065
4221
  } else {
@@ -4088,11 +4244,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
4088
4244
  return compiled.map((c, i) => {
4089
4245
  const results = opResults.get(i);
4090
4246
  if (c.type === "query") {
4091
- if (!c.query && c.predicates?.length) {
4092
- console.warn(
4093
- `[mindstudio] db.batch(): filter on '${c.config.tableName}' could not be compiled to SQL \u2014 processing in JS`
4094
- );
4095
- }
4247
+ Query._warnInJs(c, results[0]?.rows.length);
4096
4248
  return Query._processResults(results[0], c);
4097
4249
  } else if (c.type === "raw") {
4098
4250
  return RawQuery._processResults(results[0], c);
@@ -4196,11 +4348,97 @@ var init_db = __esm({
4196
4348
  init_mutation();
4197
4349
  init_raw();
4198
4350
  init_sql();
4351
+ init_time();
4199
4352
  init_table();
4200
4353
  init_raw();
4201
4354
  }
4202
4355
  });
4203
4356
 
4357
+ // src/db/readBatching.ts
4358
+ var nextTick, DbReadBatching;
4359
+ var init_readBatching = __esm({
4360
+ "src/db/readBatching.ts"() {
4361
+ "use strict";
4362
+ init_db_ws();
4363
+ nextTick = typeof setImmediate === "function" ? (fn) => setImmediate(fn) : (fn) => setTimeout(fn, 0);
4364
+ DbReadBatching = class {
4365
+ constructor(send2) {
4366
+ this.send = send2;
4367
+ }
4368
+ send;
4369
+ pending = /* @__PURE__ */ new Map();
4370
+ inFlight = /* @__PURE__ */ new Map();
4371
+ /** Run `queries` for the request holding `token`, sharing a round trip where it can. */
4372
+ async run(token, databaseId, queries) {
4373
+ const batchKey = `${token}\0${databaseId}`;
4374
+ if (queries.length === 0 || !queries.every((q) => isReadOnlySql(q.sql))) {
4375
+ this.flush(batchKey);
4376
+ return this.send(databaseId, queries);
4377
+ }
4378
+ const readKey = `${batchKey}\0${JSON.stringify(queries)}`;
4379
+ const shared = this.inFlight.get(readKey);
4380
+ if (shared) {
4381
+ return structuredClone(await shared);
4382
+ }
4383
+ const read = this.queue(batchKey, databaseId, queries);
4384
+ this.inFlight.set(readKey, read);
4385
+ try {
4386
+ return await read;
4387
+ } finally {
4388
+ this.inFlight.delete(readKey);
4389
+ }
4390
+ }
4391
+ queue(batchKey, databaseId, queries) {
4392
+ let pending = this.pending.get(batchKey);
4393
+ if (!pending) {
4394
+ pending = { databaseId, queries: [], parts: [] };
4395
+ this.pending.set(batchKey, pending);
4396
+ nextTick(() => this.flush(batchKey));
4397
+ }
4398
+ const batch = pending;
4399
+ return new Promise((resolve, reject) => {
4400
+ batch.parts.push({
4401
+ queries,
4402
+ start: batch.queries.length,
4403
+ resolve,
4404
+ reject
4405
+ });
4406
+ batch.queries.push(...queries);
4407
+ });
4408
+ }
4409
+ flush(batchKey) {
4410
+ const pending = this.pending.get(batchKey);
4411
+ if (!pending) {
4412
+ return;
4413
+ }
4414
+ this.pending.delete(batchKey);
4415
+ const { databaseId, parts } = pending;
4416
+ if (parts.length === 1) {
4417
+ this.send(databaseId, parts[0].queries).then(
4418
+ parts[0].resolve,
4419
+ parts[0].reject
4420
+ );
4421
+ return;
4422
+ }
4423
+ this.send(databaseId, pending.queries).then(
4424
+ (results) => {
4425
+ for (const part of parts) {
4426
+ part.resolve(
4427
+ results.slice(part.start, part.start + part.queries.length)
4428
+ );
4429
+ }
4430
+ },
4431
+ () => {
4432
+ for (const part of parts) {
4433
+ this.send(databaseId, part.queries).then(part.resolve, part.reject);
4434
+ }
4435
+ }
4436
+ );
4437
+ }
4438
+ };
4439
+ }
4440
+ });
4441
+
4204
4442
  // src/files/store.ts
4205
4443
  import { createHash, randomUUID } from "crypto";
4206
4444
  import { createReadStream, createWriteStream } from "fs";
@@ -6789,6 +7027,8 @@ var init_client = __esm({
6789
7027
  init_auth();
6790
7028
  init_db();
6791
7029
  init_sql();
7030
+ init_time();
7031
+ init_readBatching();
6792
7032
  init_files();
6793
7033
  init_datasources();
6794
7034
  init_voice();
@@ -8165,15 +8405,25 @@ var init_client = __esm({
8165
8405
  }
8166
8406
  return json;
8167
8407
  }
8408
+ /** @internal Reads started together share a round trip (`db/readBatching.ts`). */
8409
+ _dbReads = new DbReadBatching(
8410
+ (databaseId, queries) => this._sendDbBatch(databaseId, queries)
8411
+ );
8168
8412
  /**
8169
8413
  * @internal Execute a batch of SQL queries against a managed database.
8170
- * Used as the `executeBatch` callback for Table/Query instances.
8171
- *
8172
- * Calls `POST /_internal/v2/db/query` directly with the hook token
8173
- * (raw, no Bearer prefix). All queries run on a single SQLite connection,
8174
- * enabling RETURNING clauses and multi-statement batches.
8414
+ * Used as the `executeBatch` callback for Table/Query instances. Reads one
8415
+ * request starts in the same tick go to the platform as one batch.
8175
8416
  */
8176
8417
  async _executeDbBatch(databaseId, queries) {
8418
+ return this._dbReads.run(this._token, databaseId, queries);
8419
+ }
8420
+ /**
8421
+ * @internal Send one batch: `POST /_internal/v2/db/query` directly with the
8422
+ * hook token (raw, no Bearer prefix), or its DB WebSocket. All queries run
8423
+ * on a single SQLite connection, enabling RETURNING clauses and
8424
+ * multi-statement batches.
8425
+ */
8426
+ async _sendDbBatch(databaseId, queries) {
8177
8427
  const dbWsUrl = typeof process !== "undefined" ? process.env?.DB_WS_URL : void 0;
8178
8428
  if (dbWsUrl) {
8179
8429
  try {
@@ -8319,12 +8569,7 @@ var init_client = __esm({
8319
8569
  return new Table(tableConfig);
8320
8570
  },
8321
8571
  // Time helpers work without context
8322
- now: () => Date.now(),
8323
- days: (n) => n * 864e5,
8324
- hours: (n) => n * 36e5,
8325
- minutes: (n) => n * 6e4,
8326
- ago: (ms) => Date.now() - ms,
8327
- fromNow: (ms) => Date.now() + ms,
8572
+ ...timeHelpers,
8328
8573
  userRef: (id) => id.startsWith("@@user@@") ? id.slice("@@user@@".length) : id,
8329
8574
  // Raw SQL — validate synchronously; database resolution needs context,
8330
8575
  // so it happens inside the executeBatch closure at execution time
@@ -8641,6 +8886,44 @@ var init_client = __esm({
8641
8886
  }
8642
8887
  });
8643
8888
 
8889
+ // src/ask/context.ts
8890
+ async function fetchAskContext(params) {
8891
+ const actions = Object.entries(stepMetadata).map(([name, meta]) => ({
8892
+ name,
8893
+ description: meta.description
8894
+ }));
8895
+ let res;
8896
+ try {
8897
+ res = await fetch(`${params.baseUrl}/_internal/v2/agent/sdk/context`, {
8898
+ method: "POST",
8899
+ headers: {
8900
+ "Content-Type": "application/json",
8901
+ Authorization: `Bearer ${params.apiKey}`
8902
+ },
8903
+ body: JSON.stringify({ actions })
8904
+ });
8905
+ } catch (err) {
8906
+ throw new Error(`Network error: ${err.message}`);
8907
+ }
8908
+ if (!res.ok) {
8909
+ let message = `HTTP ${res.status}`;
8910
+ try {
8911
+ const body = await res.json();
8912
+ message = body.errorMessage ?? body.error ?? message;
8913
+ } catch {
8914
+ }
8915
+ throw new Error(`Could not load the SDK reference: ${message}`);
8916
+ }
8917
+ const { system } = await res.json();
8918
+ return system;
8919
+ }
8920
+ var init_context2 = __esm({
8921
+ "src/ask/context.ts"() {
8922
+ "use strict";
8923
+ init_metadata();
8924
+ }
8925
+ });
8926
+
8644
8927
  // src/ask/sse.ts
8645
8928
  async function* streamChat(params) {
8646
8929
  const { baseUrl, apiKey, ...body } = params;
@@ -8653,7 +8936,10 @@ async function* streamChat(params) {
8653
8936
  "Content-Type": "application/json",
8654
8937
  Authorization: `Bearer ${apiKey}`
8655
8938
  },
8656
- body: JSON.stringify(body)
8939
+ // `run`: an ask is one question and a few turns within a minute or two,
8940
+ // so the five-minute cache serves every turn after the first, at the
8941
+ // cheaper write. The route's default is the hour-long conversation cache.
8942
+ body: JSON.stringify({ ...body, cachePolicy: "run" })
8657
8943
  });
8658
8944
  } catch (err) {
8659
8945
  yield { type: "error", error: `Network error: ${err.message}` };
@@ -8718,70 +9004,140 @@ var init_sse = __esm({
8718
9004
  function kebabToCamel(s) {
8719
9005
  return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
8720
9006
  }
9007
+ function editDistance(a, b) {
9008
+ const row = Array.from({ length: b.length + 1 }, (_, j) => j);
9009
+ for (let i = 1; i <= a.length; i++) {
9010
+ let diagonal = row[0];
9011
+ row[0] = i;
9012
+ for (let j = 1; j <= b.length; j++) {
9013
+ const above = row[j];
9014
+ row[j] = Math.min(
9015
+ row[j] + 1,
9016
+ row[j - 1] + 1,
9017
+ diagonal + (a[i - 1] === b[j - 1] ? 0 : 1)
9018
+ );
9019
+ diagonal = above;
9020
+ }
9021
+ }
9022
+ return row[b.length];
9023
+ }
9024
+ function closestNames(name, names, count = 8) {
9025
+ const target = name.toLowerCase();
9026
+ return names.map((n) => {
9027
+ const lower = n.toLowerCase();
9028
+ const contains = lower.includes(target) || target.includes(lower);
9029
+ return { n, score: editDistance(target, lower) - (contains ? 100 : 0) };
9030
+ }).sort((a, b) => a.score - b.score).slice(0, count).map(({ n }) => n);
9031
+ }
9032
+ async function toolReadDoc(agent, input) {
9033
+ const id = String(input.id ?? "");
9034
+ const { baseUrl, token } = agent._httpConfig;
9035
+ const res = await fetch(
9036
+ `${baseUrl}/_internal/v2/agent/sdk/docs/${encodeURIComponent(id)}`,
9037
+ { headers: { Authorization: `Bearer ${token}` } }
9038
+ );
9039
+ if (res.status === 404) {
9040
+ return fail(`Unknown doc: ${id}. The docs are listed in <available_docs>.`);
9041
+ }
9042
+ if (!res.ok) {
9043
+ return fail(`Could not read ${id}: HTTP ${res.status}`);
9044
+ }
9045
+ const { body } = await res.json();
9046
+ return { result: body, isError: false };
9047
+ }
8721
9048
  async function toolGetActionDetails(input) {
8722
9049
  const { stepMetadata: stepMetadata2 } = await Promise.resolve().then(() => (init_metadata(), metadata_exports));
8723
- const raw = input.actionName;
9050
+ const raw = String(input.actionName ?? "");
8724
9051
  const camel = raw.includes("-") ? kebabToCamel(raw) : raw;
8725
- const meta = stepMetadata2[camel] ?? stepMetadata2[raw];
8726
- if (!meta) {
8727
- const keys = Object.keys(stepMetadata2);
8728
- const lower = camel.toLowerCase();
8729
- const match = keys.find((k) => k.toLowerCase() === lower);
8730
- if (match) {
8731
- return JSON.stringify(stepMetadata2[match], null, 2);
8732
- }
8733
- return JSON.stringify({
8734
- error: `Unknown action: ${raw}. Available actions: ${keys.slice(0, 20).join(", ")}...`
8735
- });
9052
+ const names = Object.keys(stepMetadata2);
9053
+ const name = [camel, raw].find((k) => stepMetadata2[k]) ?? names.find((k) => k.toLowerCase() === camel.toLowerCase());
9054
+ if (!name) {
9055
+ return fail(
9056
+ `Unknown action: ${raw}. The closest are: ${closestNames(camel, names).join(", ")}.`
9057
+ );
8736
9058
  }
8737
- return JSON.stringify(meta, null, 2);
9059
+ return ok(stepMetadata2[name]);
8738
9060
  }
8739
9061
  async function toolListModels(agent, input) {
8740
9062
  const type = input.type;
8741
- const details = input.details;
8742
- if (details) {
8743
- const result2 = type ? await agent.listModelsByType(type) : await agent.listModels();
8744
- return JSON.stringify(result2, null, 2);
9063
+ const modelId = input.modelId;
9064
+ if (modelId) {
9065
+ const bareId = modelId.split("@")[0];
9066
+ const { models } = type ? await agent.listModelsByType(type) : await agent.listModels();
9067
+ const model = models.find((m) => m.id === bareId);
9068
+ if (!model) {
9069
+ return fail(
9070
+ `Unknown model: ${modelId}. The closest are: ${closestNames(
9071
+ bareId,
9072
+ models.flatMap((m) => m.id ? [m.id] : [])
9073
+ ).join(", ")}.`
9074
+ );
9075
+ }
9076
+ return ok(model);
9077
+ }
9078
+ if (input.details) {
9079
+ if (!type) {
9080
+ return fail(
9081
+ "details=true needs a type. Pick the model type you are writing code for."
9082
+ );
9083
+ }
9084
+ return ok(await agent.listModelsByType(type));
8745
9085
  }
8746
- const result = type ? await agent.listModelsSummaryByType(type) : await agent.listModelsSummary();
8747
- return JSON.stringify(result, null, 2);
9086
+ return ok(
9087
+ type ? await agent.listModelsSummaryByType(type) : await agent.listModelsSummary()
9088
+ );
8748
9089
  }
8749
9090
  async function toolGetConnectorDetails(agent, input) {
8750
9091
  const serviceId = input.serviceId;
8751
9092
  const actionId = input.actionId;
8752
9093
  if (actionId) {
8753
- const result2 = await agent.getConnectorAction(serviceId, actionId);
8754
- return JSON.stringify(result2, null, 2);
9094
+ return ok(await agent.getConnectorAction(serviceId, actionId));
8755
9095
  }
8756
9096
  const result = await agent.getConnector(serviceId);
8757
- return JSON.stringify(result, null, 2);
9097
+ if (!result?.service) {
9098
+ return fail(
9099
+ `Unknown connector service: ${serviceId}. The services are listed in <connector_services>.`
9100
+ );
9101
+ }
9102
+ return ok(result);
8758
9103
  }
8759
9104
  async function executeTool(agent, name, input) {
8760
9105
  try {
8761
- let result;
8762
9106
  switch (name) {
9107
+ case "readDoc":
9108
+ return await toolReadDoc(agent, input);
8763
9109
  case "getActionDetails":
8764
- result = await toolGetActionDetails(input);
8765
- break;
9110
+ return await toolGetActionDetails(input);
8766
9111
  case "listModels":
8767
- result = await toolListModels(agent, input);
8768
- break;
9112
+ return await toolListModels(agent, input);
8769
9113
  case "getConnectorDetails":
8770
- result = await toolGetConnectorDetails(agent, input);
8771
- break;
9114
+ return await toolGetConnectorDetails(agent, input);
8772
9115
  default:
8773
- result = JSON.stringify({ error: `Unknown tool: ${name}` });
9116
+ return fail(`Unknown tool: ${name}`);
8774
9117
  }
8775
- return { result, isError: result.includes('"error"') };
8776
9118
  } catch (err) {
8777
9119
  return { result: `Error: ${err.message}`, isError: true };
8778
9120
  }
8779
9121
  }
8780
- var ASK_TOOLS;
9122
+ var ASK_TOOLS, ok, fail;
8781
9123
  var init_tools = __esm({
8782
9124
  "src/ask/tools.ts"() {
8783
9125
  "use strict";
8784
9126
  ASK_TOOLS = [
9127
+ {
9128
+ name: "readDoc",
9129
+ description: "Read one of the platform docs listed in <available_docs>, whole: the reference for one capability (task agents, files, auth, realtime events, jewels, analytics, \u2026). Read the one a question is about before answering it.",
9130
+ inputSchema: {
9131
+ type: "object",
9132
+ properties: {
9133
+ id: {
9134
+ type: "string",
9135
+ description: "The doc id, as listed in <available_docs>."
9136
+ }
9137
+ },
9138
+ required: ["id"]
9139
+ }
9140
+ },
8785
9141
  {
8786
9142
  name: "getActionDetails",
8787
9143
  description: "Get the full definition for a specific SDK action \u2014 JSON schema for inputs and outputs, usage notes, and description. Use this when you need exact parameter types, enum values, or optional fields to build correct code.",
@@ -8798,17 +9154,21 @@ var init_tools = __esm({
8798
9154
  },
8799
9155
  {
8800
9156
  name: "listModels",
8801
- description: "List available AI models, optionally filtered by type. By default returns a compact summary (id, name, type, tags). With details=true, returns full model objects including the `inputs` array that defines config options (width, height, seed, etc.) \u2014 use this when you need to check model capabilities or build code with config options. You can filter the full list yourself instead of calling this multiple times.",
9157
+ description: "List available AI models, optionally filtered by type. By default returns a compact summary (id, name, type, tags). With modelId, returns that one model in full, including the `inputs` array that defines its config options (width, height, seed, etc.): use this when you write a call with a model's config. With details=true and a type, returns every model of that type in full, which is long: use it only to compare capabilities across a type.",
8802
9158
  inputSchema: {
8803
9159
  type: "object",
8804
9160
  properties: {
9161
+ modelId: {
9162
+ type: "string",
9163
+ description: "One model to return in full, with its config inputs and serving routes."
9164
+ },
8805
9165
  type: {
8806
9166
  type: "string",
8807
- description: 'Filter by model type: "llm_chat", "image_generation", "3d_generation", "video_generation", "video_analysis", "text_to_speech", "music_generation", "lipsync", "vision", "transcription", "embedding", "reranking", "document_extraction". The last three back the data-source retrieval pipeline and have no step of their own \u2014 name them, do not pass them to a generate call.'
9167
+ description: 'Filter by model type: "llm_chat", "image_generation", "3d_generation", "video_generation", "video_analysis", "text_to_speech", "music_generation", "lipsync", "vision", "transcription", "embedding", "reranking", "document_extraction". The last three back the data-source retrieval pipeline and have no step of their own \u2014 name them, do not pass them to a generate call. Required with details=true.'
8808
9168
  },
8809
9169
  details: {
8810
9170
  type: "boolean",
8811
- description: "If true, returns full model objects with inputs/config arrays. Use this when you need to check supported inputs, config options, or model capabilities."
9171
+ description: "If true, returns full model objects with inputs/config arrays for every model of the given type."
8812
9172
  }
8813
9173
  }
8814
9174
  }
@@ -8832,492 +9192,14 @@ var init_tools = __esm({
8832
9192
  }
8833
9193
  }
8834
9194
  ];
8835
- }
8836
- });
8837
-
8838
- // src/ask/prompt/identity.ts
8839
- var identity;
8840
- var init_identity = __esm({
8841
- "src/ask/prompt/identity.ts"() {
8842
- "use strict";
8843
- identity = `
8844
- You are a senior MindStudio SDK engineer. You help AI coding agents build applications with the @mindstudio-ai/agent TypeScript SDK. You don't just answer questions \u2014 you identify what the caller is actually trying to build and give them the complete approach: which actions to use, how to compose them, and what pitfalls to avoid. Your output is consumed by developers who will implement what you propose. Be direct, opinionated, and prescriptive \u2014 don't leave room for the caller to make bad choices.
8845
-
8846
- Do not offer advice or recommend products or services outside the scope of the MindStudio SDK. If something is not possible, or would be tricky to do, note it for the developer and let them figure out how to solve it - that's outside the scope of your role.
8847
-
8848
- Never invent URLs, email addresses, support channels, Discord servers, community forums, or documentation links. Do not direct the user to "MindStudio support", "the docs", or anything similar \u2014 you do not know where those live. The only URLs you may cite are ones that appear verbatim in the reference context below. When a question is outside your scope, say so and stop \u2014 do not speculate about where else the user could get help, and do not offer a handoff path you cannot verify.
8849
-
8850
- If the user asks about @mindstudio-ai/interface, inform them that that is a separate package and they should trust what they already know about it.
8851
-
8852
- ## Scope
8853
-
8854
- 1. **Actions** \u2014 selecting and composing SDK actions for a use case
8855
- 2. **AI models** \u2014 model selection, config options, override patterns
8856
- 3. **OAuth connectors** \u2014 discovering and using the 850+ connector actions
8857
- 4. **Task agents** \u2014 autonomous multi-step tool-use loops via runTask()
8858
- 5. **Architecture** \u2014 batch execution, error handling, data flow between actions
8859
- 6. **Managed databases and auth** \u2014 db, auth, Roles, resolveUser for MindStudio apps
8860
- 7. **Realtime events** \u2014 server\u2192client push via the events namespace (publish + grant; live dashboards, notifications, chat)`.trim();
8861
- }
8862
- });
8863
-
8864
- // src/ask/prompt/reference.ts
8865
- function buildReferenceDocs(data) {
8866
- return `<sdk_reference>
8867
- <quick_reference>
8868
- Auth is always pre-configured. Use the \`mindstudio\` singleton for actions: \`import { mindstudio } from '@mindstudio-ai/agent'\`. Use \`db\`, \`auth\`, \`Roles\`, \`stream\` as direct named imports. Do NOT use \`new MindStudioAgent()\` in code examples for MindStudio apps \u2014 the singleton handles auth automatically.
8869
- Calling convention: \`const result = await mindstudio.methodName({ ...input })\`
8870
- Results are flat: output fields + \`$appId\`, \`$threadId\`, \`$billingCost\` metadata.
8871
- Thread persistence: pass \`{ threadId: result.$threadId, appId: result.$appId }\` as second arg.
8872
- All 200+ models accessed through one API key \u2014 MindStudio routes to the provider server-side.
8873
-
8874
- Execution visibility (\`onLog\`):
8875
- - **All step methods** (generateImage, generateText, searchGoogle, etc.) accept an \`onLog\` callback in the OPTIONS object (second argument). This enables SSE streaming with real-time progress during execution. Example: \`await mindstudio.generateImage({ prompt: '...' }, { onLog: (event) => console.log(event.tag, event.value) })\`. The event has \`{ value: string, tag: string, ts: number }\`. It works on ANY step method.
8876
- - **Task agents** use \`onEvent\` (on the options object itself, not a second arg) for streaming events during the tool-use loop.
8877
-
8878
- \`stream()\` \u2014 pushing updates to the frontend:
8879
- \`stream()\` sends real-time SSE events from server-side method code to the frontend. Two distinct behaviors depending on what you pass:
8880
- - \`stream('text')\` \u2192 sent as a \`token\` event. The frontend's \`onToken\` callback receives the **accumulated** text (not a delta \u2014 each call contains the full text so far). Use for streaming text output progressively.
8881
- - \`stream({ ... })\` \u2192 sent as a \`data\` event. The frontend receives this via a separate \`onStreamData\` handler (NOT \`onToken\`). Use for structured progress updates, status changes, URLs, etc.
8882
- These are fundamentally different event types. Do NOT mix them up \u2014 \`stream({ status: 'done' })\` will NOT appear in \`onToken\`, and \`stream('Processing...')\` will NOT appear in \`onStreamData\`.
8883
- \`stream()\` is silently ignored when there is no active SSE connection (CLI, background jobs) \u2014 always safe to include unconditionally.
8884
-
8885
- Progress for long-running operations:
8886
- For any step that takes more than a few seconds (video generation, image generation, audio generation, lipsync, upscaling, etc.), ALWAYS use \`onLog\` combined with \`stream()\` to keep the user informed. This is the standard pattern, not optional:
8887
- \`\`\`typescript
8888
- import { mindstudio, stream } from '@mindstudio-ai/agent';
8889
-
8890
- // Send structured progress updates (frontend reads via onStreamData)
8891
- await stream({ status: 'generating_video', message: 'Starting video generation...' });
8892
- const { videoUrl } = await mindstudio.generateVideo({
8893
- prompt: '...',
8894
- }, {
8895
- onLog: (event) => stream({ status: 'progress', message: event.value }),
8896
- });
8897
- await stream({ status: 'complete', videoUrl });
8898
- \`\`\`
8899
- Without this pattern, long-running operations appear to hang with no feedback to the user.
8900
-
8901
- Prompt caching and cost: prompt caching is automatic wherever something reads the cache back: \`runTask\` and agent loops between turns, a MindStudio agent's workflow steps within one run, and a chat between messages. A cached prefix is read at roughly 10% of full input cost; on models that bill cache writes (Claude, GPT-5.6 and later) writing it costs extra, 1.25x input for the five-minute cache and 2x for the hour-long one chats use. A single \`generateText\` call in a Remy app method isn't cached, since nothing reads it back. \`modelOverride.promptCaching: false\` turns caching off, which saves the write on a call nothing reuses (a MindStudio agent's once-a-day scheduled run, a platform-API-key call whose thread is never reused); \`promptCaching: true\` caches even a single call, for a prompt resent unchanged within minutes. The \`$billingCost\` returned on every step result already includes cache reads and writes. In multi-turn chatbot / RAG / tool-use loops, expect 40-60% of input tokens to be served from cache after the first turn. NEVER tell a user "MindStudio doesn't support prompt caching" or "the SDK doesn't expose caching" \u2014 both are wrong. The SDK does not currently surface per-call cache hit/miss counts (no \`$cacheReadTokens\` field today), but the discount IS being applied. Real cost levers for chat/RAG/tool-using agents, in priority order: (1) trim tool descriptions \u2014 JSON tool schemas are larger than people expect and add to every turn, (2) cap conversation history by turn count or token budget with summarization on overflow (single biggest variable cost driver in multi-turn chat), (3) pre-filter large contexts via embedding retrieval before passing to the expensive model rather than dumping a whole library in the prompt, (4) shorten the system prompt \u2014 every K saved is one K not in the cache footprint either. Do NOT recommend "enabling prompt caching" as a cost-reduction strategy; it's already on where it pays and the savings are already in \`$billingCost\`. The one caching lever is \`promptCaching: false\` on a call nothing reuses.
8902
-
8903
- Database queries (Query, Mutation) support both \`await\` and \`.then()/.catch()\` \u2014 use standard try/catch with await or chain \`.catch()\` directly.
8904
-
8905
- Filter predicates and bindings: Filters compile to SQL when the predicate uses field comparisons, &&/||, .includes(), null checks, and boolean fields with literal values. Predicates that reference outer-scope variables (e.g. \`o => o.companyId === input.companyId\`) cannot be compiled \u2014 JavaScript closures aren't inspectable from outside the function \u2014 so they fall back to JS and scan the whole table. For filters on tables that may grow, ALWAYS use the explicit-bindings form so the filter compiles to SQL: \`Investments.filter((i, $) => i.companyId === $.companyId, { companyId: input.companyId })\`. The second predicate parameter (any name \u2014 \`$\`, \`vars\`, \`b\`) signals that bindings are in play; the third argument provides the values. Supported on filter, findOne, count, some, every, removeAll. Bound array \u2192 \`$.ids.includes(o.field)\` compiles to IN; bound string \u2192 \`o.field.includes($.text)\` compiles to LIKE. Missing keys fall back to JS \u2014 the SDK does not substitute NULL. ALWAYS include an inline code comment when you write the bindings form so downstream coding agents don't "simplify" the bindings argument away, e.g. \`{ companyId: input.companyId } // bindings: lifts closure var so filter compiles to SQL\`.
8906
-
8907
- Database aggregation: \`count()\`, \`sum(fn)\`, \`avg(fn)\`, \`countDistinct(fn)\`, and \`aggregate(spec)\` compile to SQL aggregates (COUNT/TOTAL/AVG/GROUP BY) \u2014 no rows are fetched, so they stay cheap on tables of any size. NEVER page a whole table into memory to compute a summary; aggregate in the database. Grouped example (the spec uses STRING column names, and the filter uses bindings so it compiles to SQL):
8908
- \`\`\`typescript
8909
- const top = await Answers
8910
- .filter((a, $) => a.surveyId === $.surveyId, { surveyId }) // bindings: lifts closure var so filter compiles to SQL
8911
- .aggregate({
8912
- by: ['questionId', 'dimension'],
8913
- select: { n: { count: true }, total: { sum: 'score' }, avgScore: { avg: 'score' }, respondents: { countDistinct: 'responseId' } },
8914
- orderBy: 'total', desc: true, limit: 20,
9195
+ ok = (value) => ({
9196
+ result: JSON.stringify(value, null, 2),
9197
+ isError: false
9198
+ });
9199
+ fail = (error) => ({
9200
+ result: JSON.stringify({ error }),
9201
+ isError: true
8915
9202
  });
8916
- // Array<{ questionId, dimension, n: number, total: number, avgScore: number | null, respondents: number }>
8917
- \`\`\`
8918
- Select terms: \`{ count: true }\`, \`{ sum: 'col' }\`, \`{ avg: 'col' }\`, \`{ min: 'col' }\`, \`{ max: 'col' }\`, \`{ countDistinct: 'col' }\`. Omit \`by\` for whole-set aggregates (returns a single object). Empty-set semantics: count/countDistinct 0, sum 0, avg/min/max null. \`Table.min/max(fn)\` still return the full ROW with the min/max value; \`groupBy(fn)\` returns full rows per group and fetches everything \u2014 prefer \`aggregate()\` for summaries.
8919
-
8920
- Raw SQL escape hatch (\`db.sql\`): \`await db.sql<T>('SELECT ... FROM t WHERE x = ? ...', [val])\` runs read-only raw SQL against the app's own managed database \u2014 for joins, subqueries, and window functions the typed API can't express. SELECT/WITH only (writes throw \u2014 use Table methods). Lazy and batchable via \`db.batch()\`. LAST RESORT: prefer the typed API (including \`aggregate()\`) whenever it can express the query; raw rows may not exactly match the typed API's value representations.
8921
-
8922
- Auth + DB identity: When an app has auth enabled, the authenticated user IS a row in the app's users table. \`auth.userId\` is the row's \`id\` \u2014 do NOT add a separate \`userId\` column. Access user data with \`Users.get(auth.userId)\`. The platform creates the user row on first login and manages the \`email\`, \`phone\`, and \`roles\` columns automatically. IMPORTANT: The platform only populates the mapped auth columns (email, phone, roles) when creating the row \u2014 all other columns will be null until the developer's code sets them. Non-auth columns on the user table should be typed as optional (e.g. \`username?: string\`) and null-checked before use.
8923
-
8924
- User-typed columns: The \`User\` type is just \`string\` at runtime. Write plain UUIDs to user-typed columns \u2014 do NOT add a \`@@user@@\` prefix manually and do NOT use \`as any\`. The SDK adds and strips the prefix automatically on read/write. If TypeScript complains about assigning a plain string to a \`User\` field, use \`db.userRef(id)\` to type-cast cleanly. Reads always return bare UUIDs; use \`resolveUser(id)\` when you need display info (name, email, avatar). Never do your own prefix handling \u2014 it will fight the SDK and cause double-prefixing or orphan refs. The \`@@user@@\` prefix is only visible in storage if you bypass the SDK with raw SQL; app code should never see or produce it.
8925
-
8926
- Task agents: For multi-step tasks requiring autonomous tool use, use \`runTask()\`. Provide a prompt, input, a set of tools, an output contract, and a model. The SDK runs a tool-use loop and returns structured output. Prefer \`outputSchema\` for the output contract: plain JSON Schema (\`type\`/\`properties\`/\`required\`/\`enum\`/\`items\`; nullable via type arrays like \`["string","null"]\` \u2014 never \`nullable: true\`, never \`oneOf\`/\`$ref\`). Output is validated every turn with automatic repair and \`result.output\` is typed by inference from the schema \u2014 no generic argument and no \`parsedSuccessfully\` check needed; the call throws \`MindStudioError\` code \`task_output_schema_mismatch\` instead of ever returning nonconforming output. The legacy \`structuredOutputExample\` option (example object the model imitates) still works but is unvalidated \u2014 always check \`result.parsedSuccessfully\` before using its output. \`result.traceId\` is a stable id for the task's model transcript, which the platform records for every task (\`remy-admin tasks get <traceId>\` reads it back) \u2014 inside a jewel, also attach it to the propose/grade return (\`trace: task.traceId\`) to preserve the transcript with the pair. Tools can include any SDK action \u2014 e.g. \`['searchGoogle', 'fetchUrl', { method: 'generateImage', defaults: { imageModelOverride: { model: 'seedream-4.5' } } }]\` \u2014 plus the app's own methods (\`{ appMethod: 'save-vendor', description: 'when to use it' }\`, run as the user who invoked the method that started the task) and inline functions (\`{ name, description, inputSchema, execute }\` \u2014 the function runs in the calling process; thrown errors go back to the model as tool output; no \`defaults\`). \`onEvent\` receives each turn's events (text, thinking, tool calls and results) as the turn completes.
8927
-
8928
- Uploading files / getting a hosted URL: For app file storage (user uploads, generated documents, images, marketing assets), use the \`files\` store \u2014 define a store at module scope and call \`await SomeStore.put(bytes, { contentType?, filename? })\` \u2192 returns \`{ url }\` served on the app's own domain (private by default; use a public store for world-readable assets). \`bytes\` is a \`Buffer\`/\`Uint8Array\`, so read/fetch the content first. The older \`mindstudio.uploadFile(...)\` (and the \`mindstudio upload <filepath>\` CLI) upload to the shared account media CDN and are DEPRECATED for app storage \u2014 they remain only for account-level one-off assets (e.g. an agent avatar for \`changeProfilePicture\`). Actions that generate a file (generateImage, generateVideo, generateSpeech, generatePdf, upscaleImage, \u2026) can write their output directly into a store: pass the store handle as \`store\` in the options object, e.g. \`await mindstudio.generateImage({ prompt }, { store: Assets })\`. It's optional \u2014 omit it and the asset goes to the shared CDN as before. More generally: some SDK capabilities are exposed as \`mindstudio\` CLI commands (list-actions, run-agent, exec, batch, \u2026), not only as code \u2014 reach for the CLI for one-off operations instead of writing and running code.
8929
-
8930
- Jewel arrival triggers (\`mindstudio.jewels.propose\`): For apps whose methods carry jewels (shadow companions declared in the manifest), \`await mindstudio.jewels.propose(methodId, subject, { idempotencyKey? })\` hands a decision moment to a method's jewel at the spot where app code knows the moment was born (e.g. an ingest branch that lands a row in a pending state). The platform runs the jewel and routes by the method's autonomy; the returned \`outcome\` is one of \`recorded\` (shadow \u2014 logged for later grading), \`queued\` (approve \u2014 \`queueItemId\` addresses the item), \`committed\` (auto \u2014 the method ran; \`output\` carries its return value), \`abstained\`, \`disabled\` (no jewel / manual \u2014 returned, never thrown), \`skipped\`, \`failed\`, or \`pending\` (a concurrent replay). \`idempotencyKey\` has Stripe semantics \u2014 a replayed key returns the original outcome, so webhook retries are safe; give sibling proposals for one moment the same key (usually the row's id) so grading can close them together. Backend only (hook token). The call blocks for the jewel run \u2014 wrap chains in \`mindstudio.waitUntil(...)\` so the calling method returns immediately, and order alternatives by priority (e.g. dedupe before classify), short-circuiting when one returns \`committed\`.
8931
-
8932
- Native approval flows (\`mindstudio.jewels.queue\`): For \`approve\`-mode methods, build the review inbox in the app itself. \`queue.list({ methodId?, limit? })\` returns pending items ({ id, methodId, subject, proposed, reasoning, proposedAt, expiresAt }); \`queue.resolve(itemId, { action: 'approve' | 'dismiss', input? })\` resolves one. Approve APPLIES the target method as the current session user \u2014 the reviewer \u2014 so the effect belongs to the human who clicked and the target method's own auth checks gate who may approve; ALSO gate the wrapping list/resolve methods with the app's reviewer role (\`auth.requireRole\`). Pass \`input\` to apply an edited version of the proposal (the edit is captured with the pair \u2014 high-value signal). Dismissal records the rejection without running anything. Unresolved items expire at the method's attributionWindow. Backend only (hook token).
8933
-
8934
- Reading the app's own traffic (\`analytics\`): The platform auto-tracks pageviews (and custom events via the frontend's \`analytics.track()\`); the backend \`analytics\` namespace is the read side, so app code can correlate real traffic with its own data (lifetime views next to each blog post, "N people reading now"). One general read \u2014 \`analytics.query({ metrics, dimensions?, filters?, dateRange?, granularity?, orderBy?, limit? })\` \u2014 shaped like the Plausible query API: metrics from \`pageviews | visitors | visits | events\`, at most ONE entity dimension (path, referrerHost, country, deviceType, eventName, ...) OR \`'time'\` (+ \`granularity\`), filter triples \`[['is'|'is_not'|'contains', dimension, values]]\`, \`dateRange\` shorthand (\`'7d'\`, \`'all'\`, or \`[startISO, endISO]\`). Example \u2014 lifetime views per page: \`await analytics.query({ metrics: ['pageviews', 'visitors'], dimensions: ['path'], dateRange: 'all', limit: 10 })\` \u2192 \`results[].{ dimensions: { path }, metrics: { pageviews, visitors } }\`. IMPORTANT retention split: queries whose filters are all \`is\` and touch at most one dimension read a rollup kept forever (full history, exact uniques); cross-dimension / \`is_not\` / \`contains\` queries scan raw events retained 90 days \u2014 the server clamps the window and reports it (\`meta.source\`, \`meta.clamped\`); metrics a shape can't carry appear in \`meta.metricsOmitted\`, never as silent zeros. \`city\` filter values are plain city names (any country); with \`dateRange: 'all'\` the granularity is a minimum the server coarsens as history grows. Specials: \`analytics.live()\` (current visitor count + countries + sparkline), \`analytics.sources()\` (per-session first-source ranking, classified incl. AI assistants), \`analytics.map()\`, \`analytics.aiSources()\`, \`analytics.crawlers.overview()/timeseries()/recent()\`. Backend only (hook token); reads the LIVE app's real traffic even from a dev sandbox. Rate limited per app (600 reads/min; the SDK auto-retries a 429 once). For a page composed of several reads (KPIs + timeseries + breakdowns), use \`analytics.batch([spec, ...])\` (\u226410 queries, one round trip, one rate token each, results in order) instead of a parallel burst of \`query()\` calls. NEVER swallow \`rate_limited\` errors into empty/zero data \u2014 surface the error or retry; a page silently rendering zeros is worse than one showing a retry state.
8935
-
8936
- Email (\`email\` namespace \u2014 sending AND the app's whole delivery console): For app code, ALWAYS prefer the top-level \`email\` namespace over the legacy \`sendEmail\` action: \`import { email } from '@mindstudio-ai/agent'\` (also on the agent instance). \`email.send({ to, subject, body, category?, batchId?, ... })\` takes the same fields as the action and returns \`{ recipients, suppressed, batchId }\`. The \`sendEmail\` ACTION is the legacy v1-workflow path \u2014 still supported and running the same underlying send, but only reach for it when you need \`connectionId\` (OAuth-resolved recipients) or \`generateHtml\`; never present it as the default way to send. Key semantics: \`category\` defaults to \`'marketing'\` (per-recipient one-click unsubscribe header; unsubscribed recipients are SKIPPED and listed in \`suppressed\` \u2014 a non-empty \`suppressed\` is a success, not an error; cc/bcc are flattened into individual messages); pass \`'transactional'\` for receipts, alerts and codes (no unsubscribe header, real cc/bcc semantics, opt-outs ignored). Large and marketing sends are QUEUED: \`send()\` resolves once the send is ACCEPTED and delivery continues in the background \u2014 read progress and outcomes with \`email.batch(batchId)\` (\`status: accepted|sending|completed|partial|failed\` plus \`pending\`/chunk progress; \`status: null\` means an inline send that already finished \u2014 normal, not an error). Errors worth branching on: \`outbound_daily_cap_exceeded\` (429 \u2014 the app's daily recipient quota; check \`email.quota()\` before a big send and split the list) and \`sending_paused\` (409 \u2014 sending is paused for this app, for reputation or by support; retrying will not help). The rest of the namespace is the full observability surface \u2014 an app can build its own email admin screen and never open our dashboard: \`email.stats()\` (counts, rates, timeseries; below ~50 accepted show counts rather than percentages), \`email.messages({ status?, kind?, recipient?, batchId?, search? })\` (the per-recipient log INCLUDING mail that never reached the provider \u2014 unsubscribed, over-cap, sender-not-allowed \u2014 which is where most "it didn't send" reports land), \`email.message(id)\` (bounce diagnostics), \`email.batches()\`, and \`email.suppressions()\` / \`suppress(addr)\` / \`unsuppress(addr)\` (after an unsuppress, check \`platformSuppression\` on the result before telling anyone mail will now reach them). Backend only (hook token).
8937
-
8938
- Reporting bugs / ideas (\`reportIssue\`): For building an in-app "Report a bug" or feedback feature, use \`mindstudio.reportIssue({ title, body?, kind?, reporter? })\` (also importable directly as \`reportIssue\`). It files an issue into the app's issue tracker \u2014 visible to the app's team in the dashboard and pickup-able by the Remy agent. Backend only: wire it as frontend UI \u2192 app backend method \u2192 \`reportIssue\` (a browser can't call it; it uses the app's hook token, and the app id is derived server-side). \`kind\` is \`'bug'\` (default) or \`'idea'\`. \`reporter\` is a free-form display label (a name, email, or ticket id) \u2014 NOT an identity check and not tied to a user; pass the current user's email if you have it, or omit for anonymous. It returns the created issue \u2014 show \`result.number\` to confirm "Reported as #42". Rate limited per app (20/min): catch \`MindStudioError\` with \`code === 'rate_limited'\` and show a graceful "try again shortly" message, and debounce the submit button (every call creates a new issue \u2014 no dedupe).
8939
-
8940
- HTTP responses of the method's own (\`respond\`): A method normally returns data, which every caller gets and the API interface sends as JSON. A method whose answer is HTTP itself (TwiML for Twilio, a plain-text challenge echo for a webhook verification, a feed, a CSV, a redirect, a stored file) returns \`respond(...)\` (\`import { respond } from '@mindstudio-ai/agent'\`): \`respond({ status?, headers?, body? })\` (body a string or bytes, at most 5 MB), \`respond.text(body, { status?, headers? })\`, \`respond.json(value, { status?, headers? })\`, \`respond.redirect(url, status = 302)\`, \`respond.file(storedFile, { headers? })\`. Only the API and webhook interfaces send it; returned to any other caller (frontend method calls, MCP, agent/voice tools, crons, a streamed call) it's a method error \`http_response_not_sent\`, so keep such a method HTTP-only and wrap a shared function where the data is needed elsewhere. Headers are the app's except \`X-Remy-*\`, connection headers, \`Content-Length\` and platform cookies (\`__ms_*\`); a bad one fails as \`invalid_http_response\`. \`respond.file\` takes a store's \`put\`/\`get\`/\`list\` result and serves the file at the request's own URL straight from storage (any size, range requests work, missing \u2192 404). Failures are still thrown \`MindStudioError\`s.
8941
-
8942
- Table options: \`db.defineTable<T>(name, { unique, defaults })\`.
8943
- - \`unique: [['email'], ['userId', 'orgId']]\` \u2014 declares unique constraints (SDK communicates to platform, enables upsert).
8944
- - \`defaults: { status: 'pending' }\` \u2014 client-side defaults applied in push() and upsert().
8945
- - \`Table.upsert(conflictKey, data)\` \u2014 INSERT ... ON CONFLICT ... DO UPDATE. Conflict key must match a declared unique constraint. Returns created or updated row.
8946
- </quick_reference>
8947
-
8948
- <model_overrides>
8949
- Actions that use AI models accept a model override object. Each model has its own config options (dimensions, seed, etc.) defined in its \`inputs\` array. The \`inputs[].variable\` values are the keys for the \`config\` object:
8950
-
8951
- \`\`\`typescript
8952
- import { mindstudio } from '@mindstudio-ai/agent';
8953
-
8954
- await mindstudio.generateImage({
8955
- prompt: 'a sunset',
8956
- imageModelOverride: {
8957
- model: 'seedream-4.5',
8958
- config: {
8959
- width: 1024,
8960
- height: 768,
8961
- seed: 42,
8962
- }
8963
- }
8964
- });
8965
- \`\`\`
8966
-
8967
- Call listModels with details=true to discover the available config options for a model. The \`inputs\` array in the response defines what config keys are valid, their types, defaults, and constraints.
8968
-
8969
- Multi-vendor models list their serving routes in the \`routes\` field of listModels output. \`model: 'claude-5-opus@amazonBedrock'\` pins requests to that route (data-boundary/compliance use cases); the bare ID lets the platform route automatically and is the right default. Preserve any \`@route\` suffix the user's existing config carries.
8970
- </model_overrides>
8971
-
8972
- <actions>
8973
- ${data.llmsContent}
8974
- </actions>
8975
-
8976
- <models>
8977
- ${data.modelsSummary}
8978
- </models>
8979
-
8980
- <task_agent_models>
8981
- Models with tool-use support \u2014 the ONLY models valid for \`runTask()\`. Live from the platform catalog, so this list is always current; if a model is not here it cannot drive a tool-use loop, whatever else it is good at.
8982
-
8983
- ${data.taskModels}
8984
- </task_agent_models>
8985
-
8986
- <oauth_connections>
8987
- ${data.connections}
8988
- </oauth_connections>
8989
-
8990
- <connector_services>
8991
- OAuth connector services from the MindStudio Connector Registry. Each service has multiple actions (850+ total). Use the getConnectorDetails tool to drill into a service's actions and get input fields. Connector actions are executed via the \`runFromConnectorRegistry\` SDK action and require the user to have an OAuth connection set up for that service.
8992
-
8993
- ${data.connectorServices}
8994
- </connector_services>
8995
- </sdk_reference>`;
8996
- }
8997
- var init_reference = __esm({
8998
- "src/ask/prompt/reference.ts"() {
8999
- "use strict";
9000
- }
9001
- });
9002
-
9003
- // src/ask/prompt/instructions.ts
9004
- var instructions;
9005
- var init_instructions = __esm({
9006
- "src/ask/prompt/instructions.ts"() {
9007
- "use strict";
9008
- instructions = `<instructions>
9009
- <principles>
9010
- - Respond to intent, not just the question. When asked "how do I call generateText," also surface relevant configuration the caller probably doesn't know about \u2014 structured output options, response format controls, model-specific features. When asked "how do I parse JSON from a model response," recognize they're probably doing it wrong and suggest built-in structured output instead.
9011
- - Think at the workflow level. When the caller describes a multi-step process ("take user input, call an LLM, extract entities, save to database"), respond with the complete architectural approach: which actions to use, how to chain them, where to use batch execution, what error handling to add. Not just the signature for one action.
9012
- - Be opinionated about SDK usage. Make concrete recommendations about the right way to build things. "Use executeStepBatch here" is better than "you could optionally batch these." Stay grounded on model claims \u2014 only state facts from model metadata, not editorial judgments about quality.
9013
- - Match depth to the question. Sometimes the user will ask you for help with a single function signature, for example - or sometimes they will want complete archiecture review. A simple "what params does generateImage take" gets a concise answer with a code example. A workflow question gets the full architectural response.
9014
- </principles>
9015
-
9016
- <anti_patterns>
9017
- Flag these when the caller's question implies them:
9018
-
9019
- - **Manual JSON parsing from LLM output** \u2014 if they're calling generateText and then parsing the response, they probably want structured output / response format controls instead of \`JSON.parse(content)\`.
9020
- - **Sequential calls that should be batched** \u2014 multiple independent action calls (generate image + text-to-speech + search) should use \`executeStepBatch()\`. Three round trips become one.
9021
- - **Building custom HTTP integrations when a connector exists** \u2014 if they're asking how to call the Slack API, Airtable API, HubSpot API, etc. via \`httpRequest\`, the answer is \`runFromConnectorRegistry\` with an existing OAuth connector. 850+ connector actions exist for this. If the user hasn't configured the specific connection yet, that's step one \u2014 tell them to connect it on the platform's integrations settings page (don't guess or hardcode a URL).
9022
- - **Missing MindStudioError handling** \u2014 the SDK has structured errors with \`code\`, \`status\`, \`details\`. Catching generic \`Error\` loses actionable information. Always include \`MindStudioError\` handling in code examples.
9023
- - **One-at-a-time db writes when batch exists** \u2014 N sequential \`update()\` or \`push()\` calls should be a single \`db.batch()\` call. One round trip instead of N.
9024
- - **Closure variables in filter predicates** \u2014 \`Table.filter(o => o.x === input.x)\` cannot compile to SQL (JS closures aren't introspectable from outside the function), so it scans every row in the table in JS. For filters comparing to outer-scope values, use the explicit bindings form: \`Table.filter((o, $) => o.x === $.x, { x: input.x })\`. Recommend this whenever the predicate references a value from the enclosing function and the table might have more than a few hundred rows.
9025
- - **Hardcoded model IDs without context** \u2014 model IDs can change. When writing code with a specific model, include a comment noting which model it is and why it was chosen, so the caller can swap it later.
9026
- - **Adding a userId column to the auth users table** \u2014 \`auth.userId\` IS the row's \`id\` in the users table. The platform creates the row on login. Don't add a separate \`userId\` column \u2014 just use \`Users.get(auth.userId)\` to access the user's row.
9027
- - Do not recommend using thread persistence. This is an advanced feature - if someone needs this they will discover it on their own.
9028
- - Generate text is great for one-off text generation requests to models. For sustained conversations, conversations that require history, persistence, or multiple threads or tool use, recommend that the developer add an Agent interface to their MindStudio app and use that directly. They will understand what you mean by this.
9029
- - **Manual multi-step orchestration when runTask() fits** \u2014 if the caller is chaining multiple SDK actions with conditional logic (search \u2192 extract \u2192 generate \u2192 validate), suggest \`runTask()\` instead. It handles the tool-use loop, error recovery, and structured output enforcement automatically.
9030
- - **Recommending model switches when the user needs better prompting** \u2014 when a user says "my generated images aren't faithful enough" or "the output quality isn't good," the answer is to improve the prompt, NOT to recommend a different model. Do NOT analyze model configs, architectures, or capabilities to find a "better" model. The recommended models (Seedream 4.5, Gemini 3.1 Flash Image) handle all use cases. Help the user write a better prompt instead.
9031
- </anti_patterns>
9032
-
9033
- <task_agent_guidance>
9034
- \`runTask()\` is powerful but not always the right tool. Use this guidance to recommend it correctly:
9035
-
9036
- **USE runTask() when:**
9037
- - The task requires 3+ steps with branching or judgment (e.g. "search, and if results are bad, try different terms")
9038
- - The model needs to decide what to do next based on intermediate results (research, enrichment, content pipelines)
9039
- - Error recovery matters \u2014 if a search fails, the model can try a different approach
9040
- - The caller describes a goal, not a fixed sequence of steps
9041
- - Examples: "find contact info for this person", "research this company and create a summary with logo", "generate product content in multiple formats"
9042
-
9043
- **DO NOT use runTask() when:**
9044
- - The pipeline is simple and linear (2-3 steps, no decisions) \u2014 just call the actions directly in sequence, it's simpler and cheaper
9045
- - The task is a single action call \u2014 just use the SDK method directly (\`generateText\`, \`searchGoogle\`, etc.)
9046
- - The caller needs a persistent conversation \u2014 recommend an Agent interface in MindStudio instead
9047
- - The caller just needs text generation \u2014 use \`generateText()\` directly
9048
-
9049
- When recommending \`runTask()\`, always include: a clear prompt, the specific tools needed (not every action \u2014 just the relevant ones), and a concrete \`outputSchema\` (plain JSON Schema: type/properties/required/enum/items; nullable via type arrays). The schema is critical \u2014 it is simultaneously the model's instruction, the SDK's validation contract (validated every turn with automatic repair; throws \`task_output_schema_mismatch\` rather than returning garbage), and the TypeScript type of \`result.output\`. Reserve the legacy \`structuredOutputExample\` for deliberately loose/exploratory output, and pair it with a \`parsedSuccessfully\` check.
9050
- </task_agent_guidance>
9051
-
9052
- <model_guidance>
9053
- Each model in the reference above includes a \`popularity\` score (0.0\u20131.0) reflecting real platform usage over the last 30 days, normalized per model type. Use this to guide recommendations:
9054
-
9055
- - **When the caller asks for a specific model**: use it. But if a significantly more popular or newer model exists in the same category, mention it. "Here's how to do X with Claude 3.5 Haiku \u2014 note that Claude Haiku 4.5 is the current generation and a better default."
9056
- - **Never rank vendors** or claim one provider is better than another. Popularity reflects usage patterns, not quality judgments. Present options with their model IDs and let the caller choose. Never make assumptions about the capabilities of models, or suggest that a certain model is "good" at a specific task or better/worse than another at a task.
9057
- - **Prefer popular models as defaults** in code examples unless the caller has a reason to use something specific.
9058
- - **Always recommend latest-generation models**: For Anthropic, this is Claude 4 family, GPT-5 for OpenAI, Gemini 3 for Google, etc. MindStudio supports a ton of different models, including legacy models - but they are there for niche uses or backward compatibility/existing user requirements - they should NOT be used for new projects.
9059
- - Use the names of model input objects to infer **technical capabilities** \u2014 e.g., a model with a source image input supports image editing, a video model with start/end frame supports looping. Do not look at model tags or descriptions to determine capabilities.
9060
- - **Do NOT use config presets/enums to judge model quality or recommend models.** A model having style presets (e.g. "oil painting", "watercolor") does NOT make it better at those styles than models without presets. The recommended models (Seedream, Nano Banana Pro, etc.) are far more capable and achieve any style through prompting alone. Presets are a crutch for weaker models \u2014 never recommend a model just because it has an enum that matches the user's request.
9061
- - **Never set maxResponseTokens to a low value.** Many models use extended thinking/reasoning that counts against the token limit. Setting maxResponseTokens to 256 or 512 will cause truncated or failed responses. Set it to something high, like 16000+, even if you don't need it. Models stop generating on their own when the response is complete \u2014 a low cap does not save money, it just breaks output. ALWAYS include an inline code comment when you write this value, e.g. \`maxResponseTokens: 16000, // do not reduce \u2014 reasoning tokens count against this; lowering will truncate output\`. Without the comment, downstream coding agents will "optimize" the value down to 2000 and break the call.
9062
- - **Do NOT do deep model analysis to find "the right model for the job."** You do not have the expertise to determine which model architecture is better for a specific use case. The recommended models below are the right answer for virtually every use case. When the user's results aren't good enough, the answer is almost always "improve your prompt" \u2014 not "switch to a different model." Prompt engineering is the lever, model selection is not.
9063
- - **Do NOT hallucinate model IDs.** Only recommend models you can see in the models reference above. If you're not sure a model ID exists, do NOT use it. Call listModels to verify.
9064
- - **Never recommend a retiring or retired model.** A model marked "retires <date>, replaced by <id>" or "retired, replaced by <id>" is going away or already gone: recommend the replacement it names instead.
9065
- - **Serving-route pins**: some models can be served by more than one vendor. A model ID may carry an \`@routeKey\` suffix (e.g. \`claude-5-opus@amazonBedrock\`) that pins requests to one serving route \u2014 routes are listed in the \`routes\` field of listModels output. Pass pinned IDs through EXACTLY as given; never "correct" \`model@route\` to the bare ID. Do not add a pin yourself unless the user asks for a specific vendor \u2014 the bare ID (automatic routing) is the right default.
9066
-
9067
- ## Explicit preferences \u2014 USE THESE MODELS
9068
- MindStudio has hundreds of models. Many of them are legacy or niche. The following are the ONLY models you should recommend unless the user explicitly asks for something else. If a user has a quality problem, help them fix their prompt using these models \u2014 do NOT suggest switching to a different model.
9069
-
9070
- Text generation:
9071
- - Google Gemini, Anthropic Claude, OpenAI GPT
9072
- Image generation:
9073
- - Seedream 4.5, Google Gemini 3.1 Flash Image (Nano Banana Pro). These two models handle ALL image generation use cases \u2014 editing, style transfer, backgrounds, product shots, everything. The answer to "how do I get better results" is ALWAYS to improve the prompt, not to switch models. Never recommend Flux, DALL-E, or other legacy/niche models.
9074
- Video generation:
9075
- - Grok Imagine, Kling O3, Google Veo 3.1
9076
- Text to speech:
9077
- - Elevenlabs TTS
9078
- Image analysis:
9079
- - Prefer using a text generation model from the recommendations above - they all support image inputs
9080
-
9081
- ## Task agent models
9082
- When recommending models for \`runTask()\`, ONLY use a model from the \`<task_agent_models>\` list in the reference above. That list is the platform's own tool-use capability data, loaded fresh each time you run \u2014 so it is never out of date, and a model missing from it cannot drive a tool-use loop no matter how capable it is otherwise. Do NOT recommend a model for \`runTask()\` from memory or from the general \`<models>\` list.
9083
-
9084
- Default to \`claude-5-5-sonnet\` unless the user has a preference. Do NOT recommend o-series reasoning models (o3, o4) for tasks \u2014 they are optimized for reasoning, not tool-use loops. Do NOT recommend Gemini 2.x when 3.x is available.
9085
- </model_guidance>
9086
-
9087
- <tools>
9088
- You have 3 tools for detailed lookups. Most questions can be answered from the reference above without tools. Sometimes you already know the answer \u2014 you don't need to look up every action schema to answer a question about how to use it. Use tools when you need exact param types, model config options, or connector action details.
9089
-
9090
- - **getActionDetails(actionName)** \u2014 Full JSON schema for a specific action. Use when you need exact param types/enums to write correct code.
9091
- - **listModels(type?, details?)** \u2014 Model catalog. By default returns compact summaries. With \`details: true\`, returns full model objects including the \`inputs\` array that defines config options (width, height, seed, etc.). Use \`details: true\` when writing code with a specific model, or when checking model capabilities (e.g. which models support source images). You can filter the full response yourself \u2014 one call with details is better than many individual lookups.
9092
- - **getConnectorDetails(serviceId, actionId?)** \u2014 Drill into a connector service. With just serviceId, lists available actions. With actionId, returns the full action config with input fields for use with \`runFromConnectorRegistry\`.
9093
- </tools>
9094
-
9095
- <response_format>
9096
- - Lead with the right approach, then code. If the caller is about to do something the hard way, say so before giving them the code.
9097
- - Every response that involves code must include a complete, copy-paste-ready TypeScript example that handles the full use case \u2014 not just the one method call they asked about, but the surrounding pattern (error handling with MindStudioError, response destructuring, type annotations where helpful).
9098
- - When writing code that uses a specific model, call listModels with details=true to get the model's config options and include them.
9099
- - When building code examples, use getActionDetails to get the exact input schema first.
9100
- - After the code block, optionally list config constraints (ranges, defaults) in a compact format.
9101
- - For discovery questions ("what can I do?"), return a compact list from the reference docs.
9102
- - Assume the caller already knows what the SDK is, how to install it, and how auth works.
9103
- - Model tags in the summary are editorial labels, not technical specs. When answering questions about model capabilities (supported inputs, config options, dimensions, etc.), call listModels with details=true to check the \`inputs\` array \u2014 that is the source of truth.
9104
- - When a task agent or \`generateText\` call produces user-facing text, the prompt must include voice and tone constraints. Make sure to specify no emojis, em dashes, and other "ai-isms" in the prompt, as well as the desired tone and voice of the output.
9105
- - **Anticipate downstream coding agents second-guessing your recommendations.** Your output is consumed by another AI coding agent that will read your code, then edit it into a real codebase. That agent has its own instincts about "efficiency" and "best practices" and will silently override values that look wasteful or unusual to it \u2014 high token limits, specific model IDs that look outdated to training data, parallel calls that look redundant, prompt patterns that look verbose. When you write a value that a casual reader might "fix," include a brief inline code comment explaining the failure mode of changing it (e.g. \`// do not reduce \u2014 reasoning tokens count against this\`, \`// pinned: this model has the right tool-use behavior\`, \`// parallel intentional \u2014 these are independent\`). Comments explaining WHY a non-obvious value is correct are a deliberate exception to the usual "no comments" rule. State the consequence concretely; do not just say "do not change."
9106
- </response_format>
9107
- </instructions>`;
9108
- }
9109
- });
9110
-
9111
- // src/ask/prompt/playbook.ts
9112
- var playbook;
9113
- var init_playbook = __esm({
9114
- "src/ask/prompt/playbook.ts"() {
9115
- "use strict";
9116
- playbook = `<prompt_engineering_playbook>
9117
-
9118
- You are not just a technical assistant \u2014 you are a prompt engineering expert. When developers ask you to build something with generateText, generateImage, task agents, or multi-step workflows, you should proactively share opinions on HOW to write the prompts, not just which actions to call. Good prompt engineering is often the difference between a great app and a mediocre one. Be opinionated. Explain the techniques you're using and why.
9119
-
9120
- ## Core principles
9121
-
9122
- 1. **Role assignment beats generic instructions.** Start prompts by telling the model WHO it is acting as, not just what to do. "Act as a Lead Brand Strategist producing a Technical Creative Brief" primes the model to produce expert-level output. "Write a description of this image" does not.
9123
-
9124
- 2. **Structure beats prose.** Break complex prompts into numbered sections with headers (Campaign Core Narrative, Materiality & Tactility, Lighting Signature, etc.). Models follow structure better than they follow paragraphs.
9125
-
9126
- 3. **Explicit operational rules prevent drift.** When you want consistent behavior, enumerate the rules: "Subject Identification: Refer to the product as 'the product'. No Physical Descriptions. Product Integrity: Do not add props." Without these, models invent things.
9127
-
9128
- 4. **AI-optimized technical language.** Use precise domain terminology the model has seen in training. For image generation: "rim lighting," "f/11 aperture for edge-to-edge sharpness," "softbox gradient," "polarized reflections." For text: named frameworks, industry terms, specific examples. Avoid vague emotive words.
9129
-
9130
- 5. **Tagged context variables.** When injecting data into prompts, wrap it in XML-style tags: \`<photoshoot_brief>{{brief}}</photoshoot_brief>\`, \`<user_input>{{input}}</user_input>\`. This helps the model distinguish instructions from content.
9131
-
9132
- 6. **Output format specification at the end.** Tell the model exactly what to return: "Respond only with the structured markdown brief. Do not include introductory or concluding remarks." Or for JSON: "Respond only with a JSON array and absolutely no other text." Models default to being chatty \u2014 you have to tell them not to be.
9133
-
9134
- ## The analyze \u2192 brief \u2192 generate pattern (THE killer pattern for generation consistency)
9135
-
9136
- This is the most important pattern in MindStudio. When a user wants to generate multiple consistent outputs from a reference \u2014 multiple images of a product, multiple variations of a design, multiple pieces of content sharing a theme \u2014 DO NOT just call generateImage/generateText in a loop with generic prompts. Instead, chain actions:
9137
-
9138
- **Step 1: Analyze the reference.** Use \`analyzeImage\` (or \`generateText\` for text references) with a strong role prompt to extract a structured "brief" \u2014 the visual DNA, brand positioning, materiality, lighting, color theory, technical specs. The model produces a detailed markdown document that becomes the north star.
9139
-
9140
- **Step 2: Generate a shot list / content list.** Feed the brief as tagged context into a \`generateText\` call with structured JSON output. Ask for N distinct items that maintain the brief's consistency. Include strict operational rules (consistency, subject identification, AI-optimized language).
9141
-
9142
- **Step 3: Execute each item.** For each entry in the list, call \`generateImage\` (or \`generateText\`, etc.) passing the reference AND the specific item's prompt. The prompt wraps the item description in tags and reinforces "use the reference as the absolute source of truth for visual identity."
9143
-
9144
- ### Why this works vs a naive single call
9145
-
9146
- - A naive "generate 8 product photos" call gives you 8 random photos with no consistency.
9147
- - The analyze\u2192brief\u2192generate pattern produces a cohesive brand asset pack where every output shares the same visual DNA.
9148
- - The model has MUCH more context on the second and third steps because steps 1 and 2 packed the reference's visual identity into structured text.
9149
-
9150
- ### Example prompt snippets (use these as templates)
9151
-
9152
- **Analyze prompt (Step 1):**
9153
- \`\`\`
9154
- The provided reference image represents the "Hero Asset" of a high-end industrial design product line. Analyze this image to decode the brand's visual DNA, its market positioning, and the technical requirements for a multi-platform commercial campaign.
9155
-
9156
- Act as a Lead Brand Strategist. Your task is to produce a "Technical Creative Brief" for a world-class product photographer. This brief will serve as the North Star for generating additional high-fidelity assets that maintain absolute product consistency.
9157
-
9158
- Analyze the reference image and structure the brief as follows:
9159
-
9160
- 1. **Campaign Core Narrative:** Define the product's "vibe"...
9161
- 2. **Materiality & Tactility:** Identify the specific materials shown...
9162
- 3. **Lighting Signature:** Deconstruct the light source, quality, direction...
9163
- 4. **Color Theory & Environment:** Analyze the color palette and the "set"...
9164
- 5. **Technical Specs for Photography:** Specify focal lengths, depth of field...
9165
- 6. **Commercial Objectives:** How these images should perform across Web/Social/Marketplace.
9166
-
9167
- Respond only with the structured markdown brief. Do not include introductory or concluding remarks.
9168
- \`\`\`
9169
-
9170
- **Shot list prompt (Step 2):**
9171
- \`\`\`
9172
- <photoshoot_brief>
9173
- {{brief}}
9174
- </photoshoot_brief>
9175
-
9176
- Using the provided <photoshoot_brief>, generate a technical shot list of eight distinct, high-end commercial images. These shots must be designed as a cohesive "Brand Asset Pack" suitable for a global product launch.
9177
-
9178
- Each description must be a masterclass in technical AI prompting, focusing on how light interacts with the product's specific industrial materials while maintaining a minimalist, luxury tech aesthetic.
9179
-
9180
- ## Shot List Requirements
9181
- 1. **The Hero Centerpiece:** Front-facing, iconic shot for a website header.
9182
- 2. **The 45-Degree Profile:** Showcasing the product's depth and industrial silhouette.
9183
- 3. **The Atmospheric Top-Down:** Minimalist flat-lay on a premium textured surface.
9184
- 4. **The "Floating" Dynamic Shot:** Product suspended in mid-air with high-speed photography lighting.
9185
- ... (and so on)
9186
-
9187
- ## Strict Operational Rules
9188
- - **Subject Identification:** Refer to the product as "the product"...
9189
- - **No Physical Descriptions:** Do not describe appearance/clothing of people...
9190
- - **Product Integrity:** Do not add subjects or props that distract...
9191
- - **AI-Optimized Language:** Use technical photography terms...
9192
- - **Consistency:** Lighting temperature, background palette, and material rendering must remain identical across all shots.
9193
-
9194
- Respond only with the full shot descriptions as a JSON array and absolutely no other text.
9195
- \`\`\`
9196
-
9197
- **Generation prompt (Step 3, per item):**
9198
- \`\`\`
9199
- <new_shot_instruction>
9200
- {{shotPrompt}}
9201
- </new_shot_instruction>
9202
-
9203
- **Objective:** Using the provided reference image as the absolute source of truth for the product's industrial design, colors, and textures, generate a new, high-fidelity commercial photograph.
9204
-
9205
- **Execution Guidelines:**
9206
- 1. **Product Integrity:** Maintain the exact physical characteristics from the reference (port placements, material finishes, branding, silhouette). The product must remain "as is."
9207
- 2. **Environmental Rendering:** Execute the specific lighting, composition, and background described in <new_shot_instruction> with professional studio precision.
9208
- 3. **Commercial Fidelity:** The output must be a "market-ready" asset. Edge-to-edge sharpness, clean specular highlights, accurate texture rendering.
9209
- 4. **Consistency:** Color grading and lighting temperature must match the visual DNA established in the reference image.
9210
-
9211
- The goal is a seamless addition to the professional photoshoot that looks indistinguishable from a high-end agency production.
9212
- \`\`\`
9213
-
9214
- ## When to apply this pattern
9215
-
9216
- - **User wants multiple consistent images from a reference** (product photos, character variations, style-matched illustrations) \u2192 analyze \u2192 shot list \u2192 generate
9217
- - **User wants content in multiple formats sharing a theme** (blog + social + email from a single brief) \u2192 analyze \u2192 content plan \u2192 generate per format
9218
- - **User wants to transform a reference into a different style/context while preserving identity** \u2192 analyze identity \u2192 generate with strong reference guidance
9219
- - **User mentions "consistent," "cohesive," "branded," "matching," "on-brand"** \u2014 these are signals the analyze\u2192brief\u2192generate pattern applies
9220
-
9221
- ## When NOT to apply this pattern
9222
-
9223
- - User wants a single one-off generation (just use the action directly with a good prompt)
9224
- - User wants truly random variations (no brief needed)
9225
- - User has already written their prompts (don't override their intent)
9226
-
9227
- ## General prompt writing template
9228
-
9229
- For any generateText or analyzeImage call that produces non-trivial output:
9230
-
9231
- \`\`\`
9232
- [Role assignment: "Act as a [specific expert role]"]
9233
-
9234
- [Context: what's happening, who's consuming the output]
9235
-
9236
- [Primary task: one clear sentence]
9237
-
9238
- [Input reference: wrapped in XML tags]
9239
- <user_input>{{input}}</user_input>
9240
-
9241
- [Structured sections or numbered requirements]
9242
- 1. **Section Name:** Specific instruction
9243
- 2. **Section Name:** Specific instruction
9244
- ...
9245
-
9246
- [Operational rules / constraints]
9247
- - **Rule name:** constraint
9248
- - **Rule name:** constraint
9249
- ...
9250
-
9251
- [Output format specification]
9252
- Respond only with [exact format]. Do not include [unwanted content].
9253
- \`\`\`
9254
-
9255
- ## Voice and tone in prompts
9256
-
9257
- When the output is user-facing text (not internal analysis), ALWAYS include voice and tone constraints in the prompt. Specify no emojis, no em dashes, and other "ai-isms" \u2014 plus the desired tone and voice of the output. Without this, models default to emoji-laden corporate speak.
9258
-
9259
- ## Share opinions proactively
9260
-
9261
- When responding to prompt-related questions, be opinionated. Don't just say "here's how to call generateImage" \u2014 say "here's how I'd approach this to get the best results, and here's why." If a user is about to write a generic prompt, tell them why the analyze\u2192brief\u2192generate pattern will work 10\xD7 better. Explain the technique. Be the expert in the room.
9262
-
9263
- </prompt_engineering_playbook>`;
9264
- }
9265
- });
9266
-
9267
- // src/generated/llms-content.ts
9268
- var llms_content_exports = {};
9269
- __export(llms_content_exports, {
9270
- llmsContent: () => llmsContent
9271
- });
9272
- var llmsContent;
9273
- var init_llms_content = __esm({
9274
- "src/generated/llms-content.ts"() {
9275
- "use strict";
9276
- llmsContent = '# @mindstudio-ai/agent\n\nTypeScript SDK, CLI, and MCP server for MindStudio. One API key gives you access to 200+ AI models (OpenAI, Anthropic, Google, Meta, xAI, DeepSeek, etc.) and 1,000+ actions including 850+ connector actions across third-party services from the open-source MindStudio Connector Registry (https://github.com/mindstudio-ai/mscr). No separate provider API keys required.\n\nThis file is the complete API reference. No other documentation is needed to use the SDK.\n\n## Recommended workflow\n\nThere are 150+ actions available. Do NOT try to read or load them all at once. Follow this discovery flow:\n\n1. **Identify yourself** \u2014 Call `changeName` to set your display name (use your name or whatever your user calls you). If you have a profile picture, pass its public image URL to `changeProfilePicture`. This helps users identify your requests in their logs.\n2. **Ask** \u2014 Use `mindstudio ask "your question"` (CLI) or the `ask` MCP tool for SDK guidance. It knows every action, model, and connector and returns working TypeScript code with real model IDs and config options. Examples: `mindstudio ask "generate an image with FLUX"`, `mindstudio ask "what models support vision?"`, `mindstudio ask "how do I send a Slack message?"`.\n3. **Browse** \u2014 For manual discovery, call `listActions` (MCP tool) or `mindstudio list-actions --summary` (CLI) to get a compact `{ action: description }` map of everything available (~3k tokens). Call `mindstudio info <action>` (CLI) for parameter details.\n4. **Call it** \u2014 Invoke the action with the required parameters. All actions share the same calling convention (see below).\n\nFor specific use cases:\n\n- **OAuth third-party integrations** (Slack, Google, HubSpot, etc.): These are optional OAuth connectors from the MindStudio Connector Registry \u2014 for most tasks, use actions directly instead. If you need a third-party integration: call `listConnectors()` to browse services \u2192 `getConnectorAction(serviceId, actionId)` for input fields \u2192 execute via `runFromConnectorRegistry`. Requires an OAuth connection set up in MindStudio first \u2014 call `listConnections()` to check available connections.\n- **Pre-built agents**: Call `listAgents()` to see what\'s available \u2192 `runAgent({ appId })` to execute one. **Important:** Not all agents are configured for API use. Do not try to run an agent just because it appears in the list \u2014 only run agents the user specifically asks you to run.\n- **Model selection**: Call `listModelsSummary()` or `listModelsSummaryByType("llm_chat")` to browse models, then pass the model ID as `modelOverride.model` to actions like `generateText`. Use the summary endpoints (not `listModels`) to keep token usage low.\n- **Cost estimation**: AI-powered actions (text generation, image generation, video, audio, etc.) cost money. Call `estimateStepCost(stepType, stepInput)` before running these and confirm with the user before proceeding \u2014 unless they\'ve explicitly given permission to go ahead. Non-AI actions (data lookups, OAuth connectors, etc.) are generally free.\n- **Task agents**: For multi-step tasks that need autonomous tool use (research, content creation, data enrichment), use `runTask()`. Provide a prompt, input, tools, an output contract, and a tool-use model id. The SDK runs a tool-use loop \u2014 calling the model, executing tools, feeding results back \u2014 until structured output is produced. Tools are SDK action names, your own app methods written as `{ appMethod: "save-vendor", description: "when to use it" }`, and/or inline functions written as `{ name, description, inputSchema, execute }` \u2014 the function runs in your process when the model calls it, and a thrown error goes back to the model as tool output to work around. App methods run as the user who invoked the method that started the task, with their roles, so the agent can read and write app state on their behalf. Prefer `outputSchema` for the output contract: plain JSON Schema (`type`/`properties`/`required`/`enum`/`items`; nullable via type arrays like `["string","null"]` \u2014 no `oneOf`/`$ref`/`nullable`). Output is validated every turn with automatic repair, `result.output` is fully typed from the schema (no generic argument, no `parsedSuccessfully` check), and the call throws `MindStudioError` code `task_output_schema_mismatch` if the agent cannot produce conforming output. Example: `runTask({ prompt: "Research this restaurant and save it", input: { name: "Tartine" }, tools: ["searchGoogle", "fetchUrl", { appMethod: "saveRestaurant", description: "Persist the researched restaurant." }], outputSchema: { type: "object", properties: { name: { type: "string" }, url: { type: ["string", "null"] } }, required: ["name"] }, model: yourModelId })`. The legacy `structuredOutputExample` option (an example object the model imitates) still works but is unvalidated \u2014 check `result.parsedSuccessfully` before using its output. Pass a current tool-use model id (call `listModels` or ask). Don\'t hardcode one.\n\n## Install\n\nStandalone binary (CLI/MCP, no dependencies):\n```bash\ncurl -fsSL https://msagent.ai/install.sh | bash\n```\n\nnpm (SDK + CLI):\n```bash\nnpm install @mindstudio-ai/agent\n```\n\nRequires Node.js >= 18.\n\n## CLI\n\nThe package includes a CLI for executing steps from the command line or scripts:\n\n```bash\n# Execute with named flags (kebab-case)\nmindstudio generate-image --prompt "A mountain landscape"\n\n# Execute with JSON input (JSON5-tolerant)\nmindstudio generate-image \'{prompt: "A mountain landscape"}\'\n\n# Extract a single output field\nmindstudio generate-image --prompt "A sunset" --output-key imageUrl\n\n# List all methods (compact JSON \u2014 best for LLM discovery)\nmindstudio list --summary\n\n# List all methods (human-readable table)\nmindstudio list\n\n# Show method details (params, types, output)\nmindstudio info generate-image\n\n# Run via npx without installing\nnpx @mindstudio-ai/agent generate-text --message "Hello"\n```\n\nAuth: run `mindstudio login`, set `MINDSTUDIO_API_KEY` env var, or pass `--api-key <key>`.\nMethod names are kebab-case on the CLI (camelCase also accepted). Flags are kebab-case (`--video-url` for `videoUrl`).\nUse `--output-key <key>` to extract a single field, `--no-meta` to strip $-prefixed metadata.\n\n### Authentication\n\n```bash\n# Interactive login (opens browser, saves key to ~/.mindstudio/config.json)\nmindstudio login\n\n# Check current auth status\nmindstudio whoami\n\n# Clear stored credentials\nmindstudio logout\n```\n\nAuth resolution order: `--api-key` flag > `MINDSTUDIO_API_KEY` env > `~/.mindstudio/config.json` > `CALLBACK_TOKEN` env.\n\n## MCP server\n\nThe package includes an MCP server exposing all methods as tools. Start by calling the `listSteps` tool to discover available methods.\n\n```bash\nmindstudio mcp\n```\n\nMCP client config (standalone binary \u2014 recommended):\n```json\n{\n "mcpServers": {\n "mindstudio": {\n "command": "mindstudio",\n "args": ["mcp"],\n "env": { "MINDSTUDIO_API_KEY": "your-api-key" }\n }\n }\n}\n```\n\n## Setup\n\nInside MindStudio apps (auth is automatic):\n```typescript\nimport { mindstudio, db, auth, Roles, stream } from \'@mindstudio-ai/agent\';\n\nconst { imageUrl } = await mindstudio.generateImage({ prompt: \'a sunset\' });\n```\n\nExternal usage (API key required):\n```typescript\nimport { MindStudioAgent } from \'@mindstudio-ai/agent\';\nconst agent = new MindStudioAgent({ apiKey: \'your-key\' });\n```\n\nYour MindStudio API key authenticates all requests. MindStudio routes to the correct AI provider (OpenAI, Google, Anthropic, etc.) server-side \u2014 you do NOT need separate provider API keys.\n\n## Models\n\nDirect access to 200+ AI models from every major provider \u2014 all through a single API key, billed at cost with no markups.\n\nUse `listModels()` or `listModelsByType()` for full model details, or `listModelsSummary()` / `listModelsSummaryByType()` for a lightweight list (id, name, type, tags) suitable for LLM context windows. Pass a model ID to `modelOverride.model` in methods like `generateText` to select a specific model:\n\n```typescript\nconst { models } = await mindstudio.listModelsByType(\'llm_chat\');\nconst model = models.find(m => m.name.includes("Gemini"));\n\nconst { content } = await mindstudio.generateText({\n message: \'Hello\',\n modelOverride: {\n model: model.id,\n temperature: 0.7,\n maxResponseTokens: 16000,\n },\n});\n```\n\n## Calling convention\n\nEvery method has the signature:\n```typescript\nmindstudio.methodName(input: InputType, options?: { appId?, threadId?, onLog? }): Promise<OutputType & StepExecutionMeta>\n```\n\nThe first argument is the step-specific input object. The optional second argument controls thread/app context and debug logging.\n\n**Debug logging**: Pass `onLog` in the options to get real-time debug logs during execution (SSE streaming). Works on ALL step methods:\n```typescript\nawait mindstudio.generateImage({ prompt: \'...\' }, {\n onLog: (event) => console.log(`[${event.tag}] ${event.value}`),\n});\n// event: { value: string, tag: string, ts: number }\n```\n\n**`stream()` \u2014 push real-time updates to the frontend:**\n```typescript\nimport { mindstudio, stream } from \'@mindstudio-ai/agent\';\n\n// String \u2192 \'token\' event (frontend receives via onToken, accumulated text)\nawait stream(\'Processing...\');\n\n// Object \u2192 \'data\' event (frontend receives via onStreamData, NOT onToken)\nawait stream({ status: \'generating\', progress: 50 });\n```\nThese are different event types \u2014 do not mix them up. `stream()` is silently ignored when no SSE connection is active (safe to include unconditionally). For long-running steps, combine `onLog` with `stream()` to push progress to the frontend.\n\n**Results are returned flat** \u2014 output fields are spread at the top level alongside metadata:\n\n```typescript\nconst { content } = await mindstudio.generateText({ message: \'Hello\' });\n\n// Full result shape for any method:\nconst result = await mindstudio.generateText({ message: `Hello` });\nresult.content; // step-specific output field\nresult.$appId; // string \u2014 app ID for this execution\nresult.$threadId; // string \u2014 thread ID for this execution\nresult.$rateLimitRemaining; // number | undefined \u2014 API calls remaining in rate limit window\nresult.$billingCost; // number | undefined \u2014 cost in nanodollars (1e-9 USD; divide by 1e9 for dollars, e.g. 40000000 = $0.04)\nresult.$billingEvents; // object[] | undefined \u2014 itemized billing events\n```\n\n## Thread persistence\n\nPass `$appId`/`$threadId` from a previous result to maintain conversation state, variable state, or other context across calls:\n\n```typescript\nconst r1 = await mindstudio.generateText({ message: \'My name is Alice\' });\nconst r2 = await mindstudio.generateText(\n { message: \'What is my name?\' },\n { threadId: r1.$threadId, appId: r1.$appId },\n);\n// r2.content => "Your name is Alice"\n```\n\n## Error handling\n\nAll errors throw `MindStudioError`:\n```typescript\nimport { MindStudioError } from \'@mindstudio-ai/agent\';\n\ntry {\n await mindstudio.generateImage({ prompt: \'...\' });\n} catch (err) {\n if (err instanceof MindStudioError) {\n err.message; // Human-readable error message\n err.code; // Machine-readable code: "invalid_step_config", "api_error", "rate_limit_exceeded", "output_fetch_error"\n err.status; // HTTP status code (400, 401, 429, etc.)\n err.details; // Raw error body from the API\n }\n}\n```\n\n429 rate limit errors are retried automatically (configurable via `maxRetries`).\n\n## Low-level access\n\nFor action types not covered by generated methods:\n```typescript\nconst result = await mindstudio.executeStep(\'stepType\', { ...params });\n```\n\n## Batch execution\n\nExecute multiple steps in parallel in a single request. Maximum 50 steps per batch.\nIndividual step failures do not affect other steps \u2014 partial success is possible.\n\n```typescript\nconst result = await mindstudio.executeStepBatch([\n { stepType: \'generateImage\', step: { prompt: \'a sunset\' } },\n { stepType: \'textToSpeech\', step: { text: \'hello world\' } },\n], { appId?, threadId? });\n\n// Result:\nresult.results; // BatchStepResult[] \u2014 same order as input\nresult.results[0].stepType; // string\nresult.results[0].output; // object | undefined (step output on success)\nresult.results[0].error; // string | undefined (error message on failure)\nresult.results[0].billingCost; // number | undefined (cost in nanodollars on success)\nresult.totalBillingCost; // number | undefined \u2014 nanodollars (divide by 1e9 for USD)\nresult.appId; // string\nresult.threadId; // string\n```\n\nCLI:\n```bash\nmindstudio batch \'[{"stepType":"generateImage","step":{"prompt":"a cat"}}]\'\ncat steps.json | mindstudio batch\n```\n\n## Methods\n\nAll methods below are called on a `MindStudioAgent` instance (`agent.methodName(...)`).\nInput shows the first argument object. Output shows the fields available on the returned result.\n\n### General\n\n#### addSubtitlesToVideo\nAutomatically add subtitles to a video\n- Can control style of text and animation\n- Input: `{ videoUrl: string, language: string, fontName: string, fontSize: number, fontWeight: "normal" | "bold" | "black", fontColor: "white" | "black" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta", highlightColor: "white" | "black" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta", strokeWidth: number, strokeColor: "black" | "white" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta", backgroundColor: "black" | "white" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta" | "none", backgroundOpacity: number, position: "top" | "center" | "bottom", yOffset: number, wordsPerSubtitle: number, enableAnimation: boolean, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### addThumbnailToVideo\nSet a video\'s thumbnail: embedded as its cover art, and optionally as its opening frames\n- Almost everything that shows a video without a poster shows its first frame (a web page\'s video, link previews, the platform\'s own), so a video that fades in from black has a black thumbnail\n- Pass the thumbnail as `imageUrl` (a designed cover) or as `at`, a moment of the video in seconds\n- The thumbnail is always embedded as the video\'s cover art, its streams copied as they are; desktop players and file browsers show it, browsers and social platforms don\'t\n- `asFirstFrame` also shows it for the video\'s first 0.2 seconds, fitted to the frame over black, so first-frame thumbnails are it too; that re-encodes the video, keeping its length and audio\n- Returns the video as an mp4, and the thumbnail as `thumbnailUrl` (a png) for a web page\'s poster or a platform that takes a cover image\n- Input: `{ videoUrl: string, imageUrl?: string, at?: number, asFirstFrame?: boolean, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string, thumbnailUrl: string }`\n\n#### analyzeImage\nAnalyze one or more images with a chat model that reads images, based on a text prompt.\n- Runs on any chat model that reads images; the default is gemini-3.8-flash. `visionModelOverride` takes the same settings as a chat model\'s override (`model`, `temperature`, `maxResponseTokens`).\n- The prompt should describe what to look for or extract from the image.\n- Pass imageUrls for one or more images analyzed together in one request (imageUrl, a single image, still works).\n- Input: `{ prompt: string, imageUrl?: string, imageUrls?: string[], visionModelOverride?: { model: string, config?: object } | { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, promptCaching?: boolean, config?: object } }`\n- Output: `{ analysis: string }`\n\n#### analyzeVideo\nAnalyze a video using a video analysis model based on a text prompt.\n- The prompt describes what to look for or extract from the video.\n- The default model is gemini-3.8-flash. Any Gemini chat model reads video (the 3.x Flash and Flash Lite models, gemini-3.1-pro, and the 2.5 family), as does TwelveLabs\' pegasus-1.6; set one with `videoAnalysisModelOverride`, with `temperature` and `maxResponseTokens` beside `model`.\n- `videoUrl` is a video file\'s URL (an app file, a Google Drive share link, or any public URL). Gemini also reads a public YouTube link; Pegasus doesn\'t.\n- Length: Pegasus takes 1 second to 1 hour. Gemini takes about 3 hours at its default resolution; the 3.x models read a second of video as about 66 tokens and the 2.5 models as about 258, which is what a long video costs.\n- Input: `{ prompt: string, videoUrl: string, videoAnalysisModelOverride?: { model: string, config?: object } | { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, promptCaching?: boolean, config?: object } }`\n- Output: `{ analysis: string }`\n\n#### captureThumbnail\nCapture a thumbnail from a video at a specified timestamp\n- Input: `{ videoUrl: string, at: number | string }`\n- Output: `{ thumbnailUrl: string }`\n\n#### checkAppRole\nCheck whether the current user has a specific app role and branch accordingly.\n- Checks if the current user has been assigned a specific role in this app.\n- If the user has the role, transitions to the "has role" path.\n- If the user does not have the role, transitions to the "no role" path, or errors if no path is configured.\n- Role names are defined by the app creator and assigned to users via the app roles system.\n- The roleName field supports {{variables}} for dynamic role checks.\n- Input: `{ roleName: string, hasRoleStepId?: string, hasRoleWorkflowId?: string, noRoleStepId?: string, noRoleWorkflowId?: string }`\n- Output: `{ hasRole: boolean, userRoles: string[] }`\n\n#### convertPdfToImages\nConvert each page of a PDF document into a PNG image.\n- Each page is converted to a separate PNG.\n- The page images go to the public CDN by default, or into one of the app\'s own file stores when the call passes `store` \u2014 a private store keeps them private.\n- The source PDF is never published: one of the app\'s own files is read where it sits, and any other URL is staged privately for the renderer and discarded afterwards.\n- Returns an array of image URLs, one per page.\n- Input: `{ pdfUrl: string }`\n- Output: `{ imageUrls: string[] }`\n\n#### createDataSource\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Create a new empty vector data source for the current app.\n- Creates a new data source (vector database) associated with the current app version.\n- The data source is created empty \u2014 use the "Upload Data Source Document" block to add documents.\n- Returns the new data source ID which can be used in subsequent blocks.\n- Input: `{ name: string }`\n- Output: `{ dataSourceId: string, name: string }`\n\n#### createGmailDraft\nCreate a draft email in the connected Gmail account.\n- Requires a Google OAuth connection with Gmail compose scope.\n- The draft appears in the user\'s Gmail Drafts folder but is not sent.\n- messageType controls the body format: "plain" for plain text, "html" for raw HTML, "markdown" for auto-converted markdown.\n- Input: `{ to: string, subject: string, message: string, connectionId?: string, messageType: "plain" | "html" | "markdown" }`\n- Output: `{ draftId: string }`\n\n#### decomposeImage\nSplit an image into a base image and transparent layers.\n- Uses Seedream 5.0 Flash layer decomposition by default, and Seedream 5.0 Pro when type is "advanced", which separates more cleanly and costs more (and twice as much at 2K).\n- `resolution` is "1k", "1.5k" or "2k" (the default).\n- An optional prompt names the objects or groupings to separate. Left out, the model separates the main elements itself: text, subjects, decorations and background.\n- The output is an array of hosted PNG image URLs: first the base image at full size, then each separated element as a transparent layer cropped to the element. The layers come with no position, so placing one back over the base means finding where it sat. Flash returned 13 layers for one photo and Pro 16.\n- Input: `{ image: string, prompt?: string, type?: "standard" | "advanced", resolution?: "1k" | "1.5k" | "2k", intermediateAsset?: boolean }`\n- Output: `{ imageUrls: string[] }`\n\n#### deleteDataSource\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Delete a vector data source from the current app.\n- Soft-deletes a data source (vector database) by marking it as deleted.\n- The Milvus partition is cleaned up asynchronously by a background cron job.\n- The data source must belong to the current app version.\n- Input: `{ dataSourceId: string }`\n- Output: `{ dataSourceId: string }`\n\n#### deleteDataSourceDocument\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Delete a single document from a data source.\n- Soft-deletes a document by marking it as deleted.\n- Requires both the data source ID and document ID.\n- After deletion, reloads vectors into Milvus so the data source reflects the change immediately.\n- Input: `{ dataSourceId: string, documentId: string }`\n- Output: `{ dataSourceId: string, documentId: string }`\n\n#### detectChanges\nDetect changes between runs by comparing current input against previously stored state. Routes execution based on whether a change occurred.\n- Persists state across runs using a global variable keyed to the step ID.\n- Two modes: "comparison" (default) uses strict string inequality; "ai" uses an LLM to determine if a meaningful change occurred.\n- First run always treats the value as "changed" since there is no previous state.\n- Each mode supports transitions to different steps/workflows for the "changed" and "unchanged" paths.\n- AI mode bills normally for the LLM call.\n- Input: `{ mode: "ai" | "comparison", input: string, prompt?: string, modelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, promptCaching?: boolean, config?: object }, previousValueVariable?: string, changedStepId?: string, changedWorkflowId?: string, unchangedStepId?: string, unchangedWorkflowId?: string }`\n- Output: `{ hasChanged: boolean, currentValue: string, previousValue: string, isFirstRun: boolean }`\n\n#### detectPII\nScan text for personally identifiable information using Microsoft Presidio.\n- In workflow mode, transitions to detectedStepId if PII is found, notDetectedStepId otherwise.\n- In direct execution, returns the detection results without transitioning.\n- If entities is empty, returns immediately with no detections.\n- Input: `{ input: string, language: string, entities: string[], detectedStepId?: string, notDetectedStepId?: string, outputLogVariable?: string | null }`\n- Output: `{ detected: boolean, detections: { entity_type: string, start: number, end: number, score: number }[] }`\n\n#### downloadVideo\nDownload a video file\n- Works with YouTube, TikTok, etc., by using ytdlp behind the scenes\n- Can save as mp4 or mp3\n- Input: `{ videoUrl: string, format: "mp4" | "mp3" }`\n- Output: `{ videoUrl: string }`\n\n#### enhanceImageGenerationPrompt\nGenerate or enhance an image generation prompt using a language model. Optionally generates a negative prompt.\n- Rewrites the user\'s prompt with added detail about style, lighting, colors, and composition.\n- Also useful for initial generation, it doesn\'t always need to be enhancing an existing prompt\n- When includeNegativePrompt is true, a second model call generates a negative prompt.\n- Input: `{ initialPrompt: string, includeNegativePrompt: boolean, negativePromptDestinationVariableName?: string, systemPrompt: string, modelOverride?: unknown }`\n- Output: `{ prompt: string, negativePrompt?: string }`\n\n#### enhanceVideoGenerationPrompt\nGenerate or enhance a video generation prompt using a language model. Optionally generates a negative prompt.\n- Rewrites the user\'s prompt with added detail about style, camera movement, lighting, and composition.\n- Also useful for initial generation, it doesn\'t always need to be enhancing an existing prompt\n- When includeNegativePrompt is true, a second model call generates a negative prompt.\n- Input: `{ initialPrompt: string, includeNegativePrompt: boolean, negativePromptDestinationVariableName?: string, systemPrompt: string, modelOverride?: unknown }`\n- Output: `{ prompt: string, negativePrompt?: string }`\n\n#### evaluateDecision\nAsk structured questions about text, JSON, or images and get calibrated answers from a decision model. The model returns a probability, a selected option, or a score. It does not write a reply. Each question is a noul (yes or no, as a probability from 0 to 1), a choice (one option from a set you define, with a probability for each), or a score (a rating against ordered levels). Put every question in the same call. The question id is yours: answers come back under the same ids. The model defaults to Jev (jev-latest).\n- state is the content to judge: a string, or a JSON object or array (a record, a chat log, application state).\n- questions is a map. The key is an id you choose. It is not shown to the model. The answer comes back under the same id.\n- A noul asks a yes/no question and returns noul, a probability from 0 (no) to 1 (yes). Optional criteria.true and criteria.false say what a yes and a no mean.\n- A choice picks one option. criteria is a map of option id to a short description (or null). At most 255 options. The answer includes the winning choice, a probability for every option, and confidence.\n- A score rates the state on an ordered rubric. criteria is 2 to 10 level descriptions, low to high. The answer\'s score can land between levels, and includes a probability per level plus confidence.\n- An answer of type refusal means the model declined that question (GPT-6 Luna Decisions can).\n- instructions may be a string, or an object that keeps the question in one field and reference data in the others. Refer to those fields by name in backticks.\n- Send every question you need in one call. The model reads the state once and answers them together.\n- Probabilities from different models are not comparable: tune a threshold against the model, and the version, it will run on.\n- decisionModelOverride.model picks the model:\n- jev-latest (the default) is TypeSafe\'s current stable release, text only, up to 64k tokens. jev-preview tracks TypeSafe\'s newest release. Pin a version such as jev-1.13.0 when a threshold was tuned against that version.\n- clef and clef-flash run Cloudflare Clef, which also reads up to 4 PNG, JPEG, or WebP images, each at most 4 MiB and 8 MiB together, and takes at most 64 questions, each id using only letters, digits, underscore, dot, and hyphen, up to 100 characters. clef is the larger model, clef-flash the faster one. @cf/cloudflare/clef and @cf/cloudflare/clef-flash name the same two.\n- gpt-6-luna-decisions runs OpenAI\'s Decisions API (in beta), which reads up to 922k tokens and PNG, JPEG, WebP, or GIF images of up to 20 MB each. It takes up to 200 questions, and a choice needs at least 2 options. gpt-6-luna names it too.\n- images is for a model that reads them. Pass URLs or data URLs. A model that reads none refuses the call.\n- The response model field is the version that answered.\n- Billed per input token. Output tokens are free. Each model has its own rate, and GPT-6 Luna Decisions bills a prompt over 272k tokens at twice its rate.\n- Input: `{ state: string | object | unknown[], questions: object, images?: string[], decisionModelOverride?: { model: string, config?: object } }`\n- Output: `{ model: string, answers: object, usage: { input_tokens: number, output_tokens: number } }`\n\n#### extractAudioFromVideo\nExtract audio MP3 from a video file\n- Input: `{ videoUrl: string }`\n- Output: `{ audioUrl: string }`\n\n#### extractText\nDownload a file from a URL and extract its text content. Supports PDFs (including scanned/image-based PDFs via OCR), plain text files, and other document formats.\n- Best suited for PDFs and raw text/document files. For web pages, use the scrapeUrl step instead.\n- Handles both text-layer PDFs and image-based/scanned PDFs (e.g. Figma/Canva exports, scanned documents). Image-based PDFs are processed with OCR automatically \u2014 there is no need to convert PDF pages to images first.\n- Plain text, code, HTML, spreadsheets and Office documents (docx, pptx, odt, rtf, epub) are read without a model and cost nothing. PDFs and images go to the extraction model, billed per page.\n- Accepts a single URL, a comma-separated list of URLs, or a JSON array of URLs.\n- Nothing is published. One of the app\'s own files \u2014 including a private one, or a share link \u2014 is read where it sits; any other URL is downloaded and staged privately for the extraction model, then discarded when extraction finishes.\n- `text` is each document\'s text: a string for one URL, an array for several. `documents` has each document page by page, with its page numbers and the pages billed. Set `includeBlocks` to add each page\'s typed blocks (title, text, table, image, header, footer\u2026) with their bounding boxes, and the page\'s size.\n- A file that can\'t be read (unreachable, an unsupported type, past the model\'s limits, or refused by the model) fails the step with the reason.\n- `extractionModelOverride.model` picks the model; omit it to use the workspace\'s default:\n- `mistral-ocr-4.1` (the default): PDFs and images up to 50 MB and 1,000 pages, with typed blocks.\n- `mistral-document-ai-2512`: Mistral Document AI on Microsoft Foundry, up to 30 MB and 30 pages, with typed blocks.\n- `llamaparse`: LlamaParse\'s agentic mode, up to 100 MB and 1,000 pages, markdown without blocks.\n- `google-document-ai`: Google Document AI OCR, up to 500 pages, where the deployment has set it up.\n- Maximum file size is 50MB per URL.\n- Input: `{ url: string | string[], extractionModelOverride?: { model: string, config?: object }, model?: string, includeBlocks?: boolean }`\n- Output: `{ text: string | string[], documents: { url: string, text: string, pages: { pageNumber: number, markdown: string, blocks?: unknown[], dimensions?: { width: number, height: number, dpi?: number } }[], pagesProcessed: number }[] }`\n\n#### fetchDataSourceDocument\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Fetch the full extracted text contents of a document in a data source.\n- Loads a document by ID and returns its full extracted text content.\n- The document must have been successfully processed (status "done").\n- Also returns document metadata (name, summary, word count).\n- Input: `{ dataSourceId: string, documentId: string }`\n- Output: `{ documentId: string, name: string, content: string, summary: string, numWords: number }`\n\n#### fetchSlackChannelHistory\nFetch recent message history from a Slack channel.\n- The user is responsible for connecting their Slack workspace and selecting the channel\n- Input: `{ connectionId?: string, channelId: string, limit?: number, startDate?: string, endDate?: string, includeImages?: boolean, includeRawMessage?: boolean }`\n- Output: `{ messages: { from: string, content: string, timestamp?: string, images?: string[], rawMessage?: { agent_session?: { agent_bot_user_ids?: string[], agent_statuses?: unknown[], date_status_processing_expire?: number, status?: string, title?: string }, app_id?: string, assistant_app_thread?: { first_user_thread_reply?: string, title?: string, title_blocks?: unknown[] }, attachments?: { actions?: unknown[], app_id?: string, app_unfurl_url?: string, author_icon?: string, author_id?: string, author_link?: string, author_name?: string, author_subname?: string, blocks?: unknown[], bot_id?: string, bot_team_id?: string, callback_id?: string, channel_id?: string, channel_name?: string, channel_team?: string, color?: string, fallback?: string, fields?: unknown[], file_id?: string, filename?: string, files?: unknown[], footer?: string, footer_icon?: string, from_url?: string, hide_border?: boolean, hide_color?: boolean, id?: number, image_bytes?: number, image_height?: number, image_url?: string, image_width?: number, indent?: boolean, is_app_unfurl?: boolean, is_file_attachment?: boolean, is_msg_unfurl?: boolean, is_reply_unfurl?: boolean, is_thread_root_unfurl?: boolean, list?: unknown, list_record?: unknown, list_record_id?: string, list_records?: unknown[], list_schema?: unknown[], list_view?: unknown, list_view_id?: string, message_blocks?: unknown[], metadata?: unknown, mimetype?: string, mrkdwn_in?: string[], msg_subtype?: string, original_url?: string, pretext?: string, preview?: unknown, service_icon?: string, service_name?: string, service_url?: string, size?: number, slack_file_id?: string, text?: string, thumb_height?: number, thumb_url?: string, thumb_width?: number, title?: string, title_link?: string, ts?: string, url?: string, video_html?: string, video_html_height?: number, video_html_width?: number, video_url?: string }[], blocks?: { accessory?: unknown, alt_text?: string, api_decoration_available?: boolean, app_collaborators?: string[], app_id?: string, author_name?: string, block_id?: string, bot_user_id?: string, button_label?: string, call?: unknown, call_id?: string, description?: unknown, developer_trace_id?: string, dispatch_action?: boolean, element?: unknown, elements?: unknown[], expand?: boolean, external_id?: string, fallback?: string, fields?: unknown[], file?: unknown, file_id?: string, function_trigger_id?: string, hint?: unknown, image_bytes?: number, image_height?: number, image_url?: string, image_width?: number, is_animated?: boolean, is_workflow_app?: boolean, label?: unknown, optional?: boolean, owning_team_id?: string, provider_icon_url?: string, provider_name?: string, sales_home_workflow_app_type?: number, share_url?: string, slack_file?: unknown, source?: string, text?: unknown, thumbnail_url?: string, title?: unknown, title_url?: string, trigger_subtype?: string, trigger_type?: string, type?: unknown, url?: string, video_url?: string, workflow_id?: string }[], bot_id?: string, bot_profile?: { app_id?: string, deleted?: boolean, icons?: unknown, id?: string, name?: string, team_id?: string, updated?: number, user_id?: string }, client_msg_id?: string, display_as_bot?: boolean, edited?: { ts?: string, user?: string }, files?: { access?: string, alt_txt?: string, app_id?: string, app_name?: string, app_provides_file_work_objects?: boolean, attachments?: unknown[], blocks?: unknown[], bot_id?: string, can_toggle_canvas_lock?: boolean, canvas_creator_id?: string, canvas_printing_enabled?: boolean, canvas_readtime?: number, canvas_template_mode?: string, cc?: unknown[], channel_actions_count?: number, channel_actions_ts?: string, channels?: string[], comments_count?: number, converted_pdf?: string, created?: number, deanimate?: string, deanimate_gif?: string, display_as_bot?: boolean, dm_mpdm_users_with_file_access?: unknown[], duration_ms?: number, edit_link?: string, edit_timestamp?: number, editable?: boolean, editor?: string, editors?: string[], external_id?: string, external_type?: string, external_url?: string, favorites?: unknown[], file_access?: string, filetype?: string, from?: unknown[], groups?: string[], has_more?: boolean, has_more_shares?: boolean, has_rich_preview?: boolean, headers?: unknown, hls?: string, hls_embed?: string, id?: string, image_exif_rotation?: number, ims?: string[], initial_comment?: unknown, inline_attachment_count?: number, is_ai_suggested?: boolean, is_channel_space?: boolean, is_external?: boolean, is_modified_by_ai?: boolean, is_public?: boolean, is_restricted_sharing_enabled?: boolean, is_starred?: boolean, last_editor?: string, last_read?: number, lines?: number, lines_more?: number, linked_channel_id?: string, list_csv_download_url?: string, list_limits?: unknown, list_metadata?: unknown, media_display_type?: string, media_progress?: unknown, mimetype?: string, mode?: string, mp4?: string, mp4_low?: string, name?: string, non_owner_editable?: boolean, num_stars?: number, org_or_workspace_access?: string, original_attachment_count?: number, original_h?: string, original_w?: string, permalink?: string, permalink_public?: string, pinned_to?: string[], pjpeg?: string, plain_text?: string, pretty_type?: string, preview?: string, preview_highlight?: string, preview_is_truncated?: boolean, preview_plain_text?: string, private_channels_with_file_access_count?: number, private_file_with_access_count?: number, public_url_shared?: boolean, quip_thread_id?: string, reactions?: unknown[], saved?: unknown, sent_to_self?: boolean, shares?: unknown, show_badge?: boolean, simplified_html?: string, size?: number, skipped_shares?: boolean, source_team?: string, subject?: string, subtype?: string, team_pref_version_history_enabled?: boolean, teams_shared_with?: unknown[], template_conversion_ts?: number, template_description?: string, template_icon?: string, template_name?: string, template_title?: string, thumb_1024?: string, thumb_1024_gif?: string, thumb_1024_h?: string, thumb_1024_w?: string, thumb_160?: string, thumb_160_gif?: string, thumb_160_h?: string, thumb_160_w?: string, thumb_360?: string, thumb_360_gif?: string, thumb_360_h?: string, thumb_360_w?: string, thumb_480?: string, thumb_480_gif?: string, thumb_480_h?: string, thumb_480_w?: string, thumb_64?: string, thumb_64_gif?: string, thumb_64_h?: string, thumb_64_w?: string, thumb_720?: string, thumb_720_gif?: string, thumb_720_h?: string, thumb_720_w?: string, thumb_80?: string, thumb_800?: string, thumb_800_gif?: string, thumb_800_h?: string, thumb_800_w?: string, thumb_80_gif?: string, thumb_80_h?: string, thumb_80_w?: string, thumb_960?: string, thumb_960_gif?: string, thumb_960_h?: string, thumb_960_w?: string, thumb_gif?: string, thumb_pdf?: string, thumb_pdf_h?: string, thumb_pdf_w?: string, thumb_tiny?: string, thumb_video?: string, thumb_video_h?: number, thumb_video_w?: number, timestamp?: number, title?: string, title_blocks?: unknown[], to?: unknown[], transcription?: unknown, update_notification?: number, updated?: number, url_private?: string, url_private_download?: string, url_static_preview?: string, user?: string, user_team?: string, username?: string, vtt?: string }[], hidden?: boolean, icons?: { emoji?: string, image_36?: string, image_48?: string, image_64?: string, image_72?: string }, inviter?: string, is_locked?: boolean, language?: { is_reliable?: boolean, locale?: string }, latest_reply?: string, metadata?: { event_payload?: unknown, event_type?: string }, parent_user_id?: string, purpose?: string, reactions?: { count?: number, name?: string, url?: string, users?: string[] }[], reply_count?: number, reply_users?: string[], reply_users_count?: number, root?: { bot_id?: string, icons?: unknown, latest_reply?: string, parent_user_id?: string, reply_count?: number, reply_users?: string[], reply_users_count?: number, subscribed?: boolean, subtype?: string, text?: string, thread_ts?: string, ts?: string, type?: string, username?: string }, subscribed?: boolean, subtype?: string, team?: string, text?: string, thread_ts?: string, topic?: string, ts?: string, type?: string, upload?: boolean, user?: string, username?: string, x_files?: string[] } }[] }`\n\n#### generate3dModel\nGenerate a 3D model using a 3D generation model.\n- The default model is wavespeed-tripo3d-h3.1 (Tripo v3.1), which makes a model from the prompt, from one `image`, or from `images`: 2\u20134 views of the same object, in order front, left, back, right.\n- Models and what they take, set in `threeDModelOverride.config`:\n- From text or an image: wavespeed-tripo3d-h3.1, wavespeed-meshy7.1 (also `images`, 1\u20134 views), wavespeed-hunyuan3d-v3 (an `image`, plus optional `back_image_url`, `left_image_url`, `right_image_url`), wavespeed-hunyuan-3d-v3.1-rapid, wavespeed-tripo3d-p2, wavespeed-rodin-v2.5 (`images`, 1\u20135).\n- From an image only: trellis-2, wavespeed-meshy7 (or `images`, 1\u20134 views), wavespeed-tripo3d-v2.5, seed3d-2.0, wavespeed-sam-3d-objects (with optional `mask_images`).\n- From views: wavespeed-hi3d-v3.0 (`front_image`, and optional `back_image`, `left_image`, `right_image`).\n- `$mode` in the config picks a model\'s mode (`text-to-3d`, `image-to-3d`, or its multi-view mode). Left out, `images` picks the multi-view mode and `image` picks image-to-3d; with neither, the model\'s default mode. A text mode needs the prompt; in the others the prompt is optional guidance.\n- The output is standardized as a GLB URL plus optional FBX/OBJ/USDZ, thumbnail, texture maps, and provider task ID.\n- Input: `{ prompt?: string, intermediateAsset?: boolean, threeDModelOverride?: { model: string, config?: object } }`\n- Output: `{ prompt?: string, glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], providerTaskId?: string, resolvedConfig?: object }`\n\n#### generateAsset\nGenerate an HTML asset and export it as a webpage, PDF, or image\n- Agents can generate HTML documents and export as webpage, PDFs, images, or videos. They do this by using the "generatePdf" block, which defines an HTML page with variables, and then the generation process renders the page to create the output and save its URL at the specified variable.\n- The template for the HTML page is generated by a separate process, and it can only use variables that have already been defined in the workflow at the time of its execution. It has full access to handlebars to render the HTML template, including a handlebars helper to render a markdown variable string as HTML (which can be useful for creating templates that render long strings). The template can also create its own simple JavaScript to do things like format dates and strings.\n- If PDF or composited image generation are part of the workflow, assistant adds the block and leaves the "source" empty. In a separate step, assistant generates a detailed request for the developer who will write the HTML.\n- Can also auto-generate HTML from a prompt (like a generate text block to generate HTML). In these cases, create a prompt with variables in the dynamicPrompt variable describing, in detail, the document to generate\n- Can either display output directly to user (foreground mode) or save the URL of the asset to a variable (background mode)\n- Input: `{ source: string, sourceType: "html" | "markdown" | "spa" | "raw" | "dynamic" | "customInterface", outputFormat: "pdf" | "png" | "html" | "mp4" | "openGraph", pageSize: "full" | "letter" | "A4" | "custom", testData: object, options?: { pageWidthPx?: number, pageHeightPx?: number, pageOrientation?: "portrait" | "landscape", rehostMedia?: boolean, videoDurationSeconds?: number }, spaSource?: { source?: string, lastCompiledSource?: string, files?: object, paths: string[], root: string, zipUrl: string }, rawSource?: string, dynamicPrompt?: string, dynamicSourceModelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, promptCaching?: boolean, config?: object }, transitionControl?: "default" | "native", shareControl?: "default" | "hidden", shareImageUrl?: string, intermediateAsset?: boolean }`\n- Output: `{ url: string }`\n\n#### generateImage\nGenerate an image from a text prompt, or edit one, using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences).\n- The default model is seedream-4.5. Options go in `imageModelOverride.config` under the model\'s own names, and differ by model, so look the model up before writing the call. Images to edit or draw from go there too, as URLs: `images` on most models, `source_images` on the Gemini and GPT Image models, one `image` on qwen-image, z-image-turbo, krea-v2-turbo, ideogram-v4 and the MAI models, `source_image` on Grok Imagine, `image_url` on the Ideogram remix and FLUX Kontext models, and `reference` on krea-v2-large.\n- A model that makes and edits images by separate means takes `$mode` in its config (`text-to-image`, and `edit`, `image-edit` or `image-to-image`, as it lists). Left out, images in the config pick the edit mode, and otherwise the model makes the image from the prompt.\n- Images are hosted on the public CDN by default, or in one of the app\'s own file stores when the call passes `store` \u2014 a private store keeps the image private.\n- In foreground mode, the image is displayed to the user. In background mode, the URL is saved to a variable.\n- When generateVariants is true with numVariants > 1 (up to 10), the images are generated in parallel. One that fails is left out of the URLs returned, and the step fails only when every one does.\n- In direct execution, foreground mode behaves as background, and userSelect variant behavior behaves as saveAll.\n- Input: `{ prompt: string, intermediateAsset?: boolean, imageModelOverride?: { model: string, config?: object }, generateVariants?: boolean, numVariants?: number, addWatermark?: boolean }`\n- Output: `{ imageUrl: string | string[] }`\n\n#### generateLipsync\nGenerate a lip sync video from audio and an image or video.\n- In foreground mode, the video is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ intermediateAsset?: boolean, addWatermark?: boolean, lipsyncModelOverride?: { model: string, config?: object } }`\n- Output: `{ videoUrl: string }`\n\n#### generateMusic\nGenerate an audio file from provided instructions (text) using a music model.\n- `text` describes the song: genre, mood, instruments, tempo, vocals, and for Lyria its structure (`[Verse]`, `[Chorus]`, timestamps such as `[0:00-0:20] Intro`).\n- The default model is elevenlabs-music_v2.5. Options go in `musicModelOverride.config`, and differ by model:\n- elevenlabs-music_v2.5, elevenlabs-music_v2: `musicLengthMs`, the song\'s length from 3,000 to 600,000 (the model chooses one from the text otherwise, and holds a length it\'s given), and `forceInstrumental: true` for no vocals.\n- lyria-3.5: songs of up to about three minutes, their length steered from the text; `image`, the URL of an image for the song to take its mood and subject from. lyria-3 makes 30-second clips, lyria-3-pro songs of up to about three minutes.\n- minimax-music-3.0: `lyrics` (written from the text when left out), `is_instrumental: true`, `bitrate`, `sample_rate`. minimax-music-2.5 requires `lyrics`.\n- In foreground mode, the audio is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ text: string, intermediateAsset?: boolean, musicModelOverride?: { model: string, config?: object } }`\n- Output: `{ audioUrl: string }`\n\n#### generateSound\nGenerate a sound effect from a text description using a sound model.\n- `text` describes the sound effect to generate.\n- The default model is elevenlabs-sound_v2. Options go in `soundModelOverride.config`:\n- `durationSeconds` \u2014 the length, from 0.5 to 30. Left out, the model picks one from the text, and the effect is billed as 5 seconds whatever its length.\n- `loop: true` \u2014 a sound that repeats with no audible start or end, for ambience and backgrounds.\n- `promptInfluence` \u2014 how literally the text is followed, from 0 to 1 (default 0.3).\n- `outputFormat` \u2014 MP3 (`mp3_44100_128` by default, up to `mp3_44100_192`) or Opus (`opus_48000_32` to `opus_48000_192`).\n- In foreground mode, the audio is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ text: string, intermediateAsset?: boolean, soundModelOverride?: { model: string, config?: object } }`\n- Output: `{ audioUrl: string }`\n\n#### generateStaticVideoFromImage\nConvert a static image to an MP4\n- Can use to create slides/intertitles/slates for video composition\n- Input: `{ imageUrl: string, duration: string }`\n- Output: `{ videoUrl: string }`\n\n#### generateText\nSend a message to an AI model and return the response, or echo a system message.\n- Source "user" sends the message to an LLM and returns the model\'s response.\n- Source "system" echoes the message content directly (no AI call).\n- Mode "background" saves the result to a variable. Mode "foreground" streams it to the user (not available in direct execution).\n- Structured output (JSON/CSV) can be enforced via structuredOutputType and structuredOutputExample.\n- When executed inside a v2 app method (managed sandbox or local dev tunnel),\nLLM token output can be streamed to the frontend in real time via an SSE\nside-channel. The frontend opts in by passing { stream: true } to the method\ninvocation via @mindstudio-ai/interface. Tokens are published to Redis\npub/sub as they arrive and forwarded as SSE events on the invoke response.\nThe method code itself is unchanged \u2014 streaming is transparent to the\ndeveloper. See Sandboxes/execute.ts and the invoke handler in V2Apps for\nthe server-side plumbing.\n- Input: `{ message: string, source?: "user" | "system", modelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, promptCaching?: boolean, config?: object }, structuredOutputType?: "text" | "json" | "csv", structuredOutputExample?: string, chatHistoryMode?: "include" | "exclude" }`\n- Output: `{ content: string }`\n\n#### generateVideo\nGenerate a video from a text prompt, an image or references, using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences): the subject, the motion, the camera and the sound.\n- The default model is wan3.0-video. Options go in `videoModelOverride.config` under the model\'s own names, and differ by model, so look the model up before writing the call. Images, videos and audio go there too, as URLs: a first frame is `image` on most models, `image_url` on Veo, gemini-omni-flash, Runway\'s and minimax-h3-max, `source_image` on Grok Imagine, `start_frame` on ray-2 and `first_frame_image` on skyreels-v4.\n- A model that makes video more than one way takes `$mode` in its config (`text-to-video`, `image-to-video`, `reference-to-video`, `video-edit` and others it lists). Left out, a first frame or reference images in the config pick their mode, and otherwise the model\'s default runs.\n- Videos are hosted on the public CDN by default, or in one of the app\'s own file stores when the call passes `store` \u2014 a private store keeps the video private, with no public copy left behind.\n- In foreground mode, the video is displayed to the user. In background mode, the URL is saved to a variable.\n- When generateVariants is true with numVariants > 1, multiple videos are generated in parallel.\n- In direct execution, foreground mode behaves as background, and userSelect variant behavior behaves as saveAll.\n- Input: `{ prompt: string, intermediateAsset?: boolean, videoModelOverride?: { model: string, config?: object }, generateVariants?: boolean, numVariants?: number, addWatermark?: boolean }`\n- Output: `{ videoUrl: string | string[] }`\n\n#### getGmailAttachments\nDownload attachments from a Gmail email and store them on the app\'s own storage.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Attachments are stored and returned as URLs \u2014 on the public CDN by default, or in one of the app\'s own file stores when the call passes `store`. Use a private store for anything confidential; a public CDN URL is readable by anyone who has it.\n- Attachments larger than 25MB are skipped.\n- Use the message ID from Search Gmail Emails, List Recent Gmail Emails, or Get Gmail Email steps.\n- Input: `{ messageId: string, connectionId?: string }`\n- Output: `{ attachments: { filename: string, mimeType: string, size: number, url: string }[] }`\n\n#### getGmailUnreadCount\nGet the number of unread emails in the connected Gmail inbox.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns the unread message count for the inbox label.\n- This is a lightweight call that does not fetch any email content.\n- Input: `{ connectionId?: string }`\n- Output: `{ unreadCount: number }`\n\n#### getMediaMetadata\nGet info about a media file\n- Input: `{ mediaUrl: string }`\n- Output: `{ metadata: string }`\n\n#### imageFaceSwap\nReplace a face in an image with a face from another image using AI.\n- Requires both a target image and a face source image.\n- Output is a PNG on the public CDN, or in one of the app\'s own file stores when the call passes `store` \u2014 a private store keeps it private.\n- Input: `{ imageUrl: string, faceImageUrl: string, engine: string }`\n- Output: `{ imageUrl: string }`\n\n#### imageRemoveWatermark\nRemove watermarks from an image using AI.\n- Output is a PNG on the public CDN, or in one of the app\'s own file stores when the call passes `store` \u2014 a private store keeps it private.\n- Input: `{ imageUrl: string, engine: string, intermediateAsset?: boolean }`\n- Output: `{ imageUrl: string }`\n\n#### importFromUrl\nCopy a file at a URL into one of the app\'s file stores, without the bytes passing through the method\n- For a direct link to a file (a recording, an export, a podcast episode\'s mp3), up to 5 GiB. The file is fetched and stored as it is.\n- Requires a store: pass the store handle as `store` in the options. `Store.importFromUrl(url)` is the same call, returning a StoredFile.\n- A share page (a Google Drive or Dropbox page, YouTube) is not the file: pass the file\'s own download link, or use downloadVideo for a hosted video.\n- Input: `{ url: string, contentType?: string }`\n- Output: `{ fileUrl: string, key: string, size: number, contentType: string }`\n\n#### insertVideoClips\nInsert b-roll clips into a base video at a timecode, optionally with an xfade transition.\n- Input: `{ baseVideoUrl: string, overlayVideos: { videoUrl: string, startTimeSec: number }[], transition?: string, transitionDuration?: number, useOverlayAudio?: boolean, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### listDataSources\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. List all data sources for the current app.\n- Returns metadata for every data source associated with the current app version.\n- Each entry includes the data source ID, name, description, status, and document list.\n- Input: `object`\n- Output: `{ dataSources: ({ id: string, name: string, description: string, status: "loaded" | "hibernating", dateCreated: string, documents: ({ id: string, name: string, status: "processing" | "done" | "error", numWords?: number, dateUploaded: string })[] })[] }`\n\n#### listGmailLabels\nList all labels in the connected Gmail account. Use these label IDs or names with the Update Gmail Labels step.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns both system labels (INBOX, SENT, TRASH, etc.) and user-created labels.\n- Label type is "system" for built-in labels or "user" for custom labels.\n- Input: `{ connectionId?: string }`\n- Output: `{ labels: { id: string, name: string, type: string }[] }`\n\n#### listRecentGmailEmails\nList recent emails from the connected Gmail inbox.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns up to 100 emails (default 5), ordered by most recent first.\n- Functionally equivalent to Search Gmail Emails with an "in:inbox" query.\n- Input: `{ connectionId?: string, exportType: "json" | "text", limit: string }`\n- Output: `{ emails: { id: string, subject: string, from: string, to: string, date: string, plainBody: string, htmlBody: string, labels: string }[] }`\n\n#### logic\nRoute execution to different branches based on AI evaluation, comparison operators, or workflow jumps.\n- Supports two modes: "ai" (default) uses an AI model to pick the most accurate statement; "comparison" uses operator-based checks.\n- In AI mode, the model picks the most accurate statement from the list. All possible cases must be specified.\n- In comparison mode, the context is the left operand and each case\'s condition is the right operand. First matching case wins. Use operator "default" as a fallback.\n- Requires at least two cases.\n- Each case can transition to a step in the current workflow (destinationStepId) or jump to another workflow (destinationWorkflowId).\n- Input: `{ mode?: "ai" | "comparison", context: string, cases: ({ id: string, condition: string, operator?: "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "exists" | "not_exists" | "contains" | "not_contains" | "default", destinationStepId?: string, destinationWorkflowId?: string } | string)[], modelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, promptCaching?: boolean, config?: object } }`\n- Output: `{ selectedCase: number }`\n\n#### makeDotComRunScenario\nTrigger a Make.com (formerly Integromat) scenario via webhook and return the response.\n- The webhook URL must be configured in your Make.com scenario.\n- Input key-value pairs are sent as JSON in the POST body.\n- Response format depends on the Make.com scenario configuration.\n- Input: `{ webhookUrl: string, input: object }`\n- Output: `{ data: unknown }`\n\n#### mergeAudio\nMerge one or more clips into a single audio file.\n- Input: `{ mp3Urls: string[], fileMetadata?: object, albumArtUrl?: string, intermediateAsset?: boolean }`\n- Output: `{ audioUrl: string }`\n\n#### mergeVideos\nMerge one or more clips into a single video.\n- Input: `{ videoUrls: string[], transition?: string, transitionDuration?: number, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### mixAudioIntoVideo\nMix an audio track into a video\n- Input: `{ videoUrl: string, audioUrl: string, options: { keepVideoAudio?: boolean, audioGainDb?: number, videoGainDb?: number, loopAudio?: boolean }, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### muteVideo\nMute a video file\n- Input: `{ videoUrl: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### n8nRunNode\nTrigger an n8n workflow node via webhook and return the response.\n- The webhook URL must be configured in your n8n workflow.\n- Supports GET and POST methods with optional Basic authentication.\n- For GET requests, input values are sent as query parameters. For POST, they are sent as JSON body.\n- Input: `{ method: string, authentication: "none" | "basic" | "string", user: string, password: string, webhookUrl: string, input: object }`\n- Output: `{ data: unknown }`\n\n#### postToSlackChannel\nSend a message to a Slack channel via a connected bot.\n- The user is responsible for connecting their Slack workspace and selecting the channel\n- Supports both simple text messages and slack blocks messages\n- Text messages can use limited markdown (slack-only fomatting\u2014e.g., headers are just rendered as bold)\n- Input: `{ channelId: string, messageType: "string" | "blocks", message: string, connectionId?: string }`\n- Output: `unknown`\n\n#### postToZapier\nSend data to a Zapier Zap via webhook and return the response.\n- The webhook URL must be configured in the Zapier Zap settings\n- Input keys and values are sent as the JSON body of the POST request\n- The webhook response (JSON or plain text) is returned as the output\n- Input: `{ webhookUrl: string, input: object }`\n- Output: `{ data: unknown }`\n\n#### queryDataSource\nLegacy 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.\n- Queries a vectorized data source and returns the most relevant chunks.\n- Useful for retrieval-augmented generation (RAG) workflows.\n- Input: `{ dataSourceId: string, query: string, maxResults: number }`\n- Output: `{ text: string, chunks: string[], query: string, citations: unknown[], latencyMs: number }`\n\n#### queryExternalDatabase\nExecute a SQL query against an external database connected to the workspace.\n- Requires a database connection configured in the workspace.\n- Supports PostgreSQL (including Supabase), MySQL, and MSSQL.\n- Results can be returned as JSON or CSV.\n- Input: `{ connectionId?: string, query: string, outputFormat: "json" | "csv" }`\n- Output: `{ data: unknown }`\n\n#### redactPII\nReplace personally identifiable information in text with placeholders using Microsoft Presidio.\n- PII is replaced with entity type placeholders (e.g. "Call me at <PHONE_NUMBER>").\n- If entities is empty, returns empty text immediately without processing.\n- Input: `{ input: string, language: string, entities: string[] }`\n- Output: `{ text: string }`\n\n#### removeBackgroundFromImage\nRemove the background from an image using AI, producing a transparent PNG.\n- Uses the Bria background removal model via fal.ai by default.\n- Uses WaveSpeed\'s Ideogram background removal model when type is "advanced".\n- Output is a PNG with transparency on the public CDN, or in one of the app\'s own file stores when the call passes `store` \u2014 a private store keeps it private, `autoCrop` included.\n- Input: `{ type?: "standard" | "advanced", imageUrl: string, autoCrop?: boolean }`\n- Output: `{ imageUrl: string }`\n\n#### renderVideoFromHtmlComposition\nRender an HTML composition (HTML, CSS, media and seekable animation on a timeline) to a video file\n- Renders an HTML composition in HyperFrames\' format (https://hyperframes.heygen.com) with HyperFrames 0.8.143, in Chrome off the app\'s own sandbox; author for that version\n- Pass the composition as `html`, the page itself (up to 10 MB), or as `project`, the URL of a zip of a project\'s folder (`index.html` at its root, or the file `entryFile` names) or of one HTML file, up to 2 GiB: an app file or any public URL\n- `variables` overrides the composition\'s `data-composition-variables` defaults, which it reads with `window.__hyperframes.getVariables()`\n- Whatever the composition loads itself (fonts, scripts, images, video) is fetched as it renders, so each is in the project or at a public URL; a private app file needs a signed URL\n- fps 24, 30 or 60 (default 30); quality draft, standard or high (default standard); format mp4 (default), webm or mov (both keep transparency), or gif\n- An mp4, webm or mov renders in chunks of about 10 seconds side by side, so a long video takes little longer than a short one; a call finishes within about 30 minutes, and reports its progress as it goes. A gif renders whole, within 15 minutes\n- A composition HyperFrames can\'t render fails with HyperFrames\' own reason\n- Input: `{ html?: string, project?: string, entryFile?: string, variables?: object, fps?: 24 | 30 | 60, quality?: "draft" | "standard" | "high", format?: "mp4" | "webm" | "mov" | "gif", intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string, durationSec: number, frames: number }`\n\n#### resizeVideo\nResize a video file\n- `mode` is required: `fit` scales the video down to fit within `maxWidth` x `maxHeight`, keeping its shape (a video already inside them is left that size); `exact` makes it `width` x `height`, padding (`strategy: \'pad\'`, the default) or cropping (`\'crop\'`) where the shapes differ\n- Input: `{ videoUrl: string, mode: "fit" | "exact", maxWidth?: number, maxHeight?: number, width?: number, height?: number, strategy?: "pad" | "crop", intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### runFfmpeg\nRun an ffmpeg command on media files\n- Runs one ffmpeg command, its arguments exactly as given, off the app\'s own sandbox (ffmpeg 9, a static build with the common codecs and filters)\n- `inputs` names each input file by URL; `{in:<name>}` anywhere in an argument, a filter graph included, is that file\'s local path\n- `{out:<file.ext>}` anywhere in an argument is an output\'s path; every output the arguments name comes back in `outputs` by its file name, its type from its extension\n- One command per call, finishing within 15 minutes; up to 10 GB of inputs and outputs together, 5 GB per output\n- The system fonts are Noto Sans (`drawtext`\'s default), DejaVu Sans, Liberation Sans and Noto Color Emoji: pass a font file as an input for any other\n- A command ffmpeg refuses fails with ffmpeg\'s own error lines\n- Input: `{ inputs: object, args: string[], intermediateAsset?: boolean }`\n- Output: `{ outputs: object }`\n\n#### runFromConnectorRegistry\nRun a raw API connector to a third-party service\n- Use the /developer/v2/helpers/connectors endpoint to list available services and actions.\n- Use /developer/v2/helpers/connectors/{serviceId}/{actionId} to get the full input configuration for an action.\n- Use /developer/v2/helpers/connections to list your available OAuth connections.\n- The actionId format is "serviceId/actionId" (e.g., "slack/send-message").\n- Pass a __connectionId to authenticate the request with a specific OAuth connection, otherwise the default will be used (if configured).\n- Input: `{ actionId: string, displayName: string, icon: string, configurationValues: object, __connectionId?: string }`\n- Output: `{ data: object }`\n\n#### scrapeLinkedInCompany\nScrape public company data from a LinkedIn company page.\n- Requires a LinkedIn company URL (e.g. https://www.linkedin.com/company/mindstudioai).\n- Returns structured company data including description, employees, updates, and similar companies.\n- Input: `{ url: string }`\n- Output: `{ company: unknown }`\n\n#### scrapeLinkedInProfile\nScrape public profile data from a LinkedIn profile page.\n- Requires a LinkedIn profile URL (e.g. https://www.linkedin.com/in/username).\n- Returns structured profile data including experience, education, articles, and activities.\n- Input: `{ url: string }`\n- Output: `{ profile: unknown }`\n\n#### scrapeUrl\nExtract text, HTML, or structured content from one or more web pages.\n- Accepts a single URL or multiple URLs (as a JSON array, comma-separated, or newline-separated).\n- Output format controls the result shape: "text" returns markdown, "html" returns raw HTML, "json" returns structured scraper data, "summary" returns a model-written summary and requires the "firecrawl" service.\n- Can optionally capture a screenshot of each page. Screenshots go to the public CDN by default, or into one of the app\'s own file stores when the call passes `store`.\n- Handles bot protection automatically; no proxy or rendering configuration is needed.\n- Input: `{ url: string, service?: "default" | "firecrawl", autoEnhance?: boolean, outputFormat?: "text" | "json" | "html" | "summary", pageOptions?: { onlyMainContent: boolean, screenshot: boolean, waitFor: number, replaceAllPathsWithAbsolutePaths: boolean, headers: object, removeTags: string[], mobile: boolean } }`\n- Output: `{ content: string | string[] | { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string }, error?: { code: string, message: string }, costUnits?: number } | { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string }, error?: { code: string, message: string }, costUnits?: number }[], screenshot?: string | string[] }`\n\n#### scrapeXPost\nScrape data from a single X (Twitter) post by URL.\n- Returns structured post data (text, html, optional json/screenshot/metadata).\n- Optionally saves the text content to a variable.\n- Input: `{ url: string }`\n- Output: `{ post: { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string }, error?: { code: string, message: string }, costUnits?: number } }`\n\n#### scrapeXProfile\nScrape public profile data from an X (Twitter) account by URL.\n- Returns structured profile data.\n- Optionally saves the result to a variable.\n- Input: `{ url: string }`\n- Output: `{ profile: { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string }, error?: { code: string, message: string }, costUnits?: number } }`\n\n#### screenshotUrl\nCapture a screenshot of a web page as a PNG image.\n- Takes a viewport or full-page screenshot of the given URL.\n- Returns a PNG image URL: on the public CDN by default, or in one of the app\'s own file stores when the call passes `store` \u2014 a private store keeps the screenshot private.\n- Viewport mode captures only the visible area; fullPage captures the entire scrollable page, scrolling it first so lazy-loaded content is included.\n- Works on sites behind bot protection (Cloudflare and similar).\n- Cookie-consent banners and similar overlays are suppressed automatically.\n- `width` and `height` are the exact output pixel dimensions.\n- You can add a delay or wait for a CSS selector before capturing. A selector that never appears fails the step rather than capturing early.\n- Input: `{ url: string, mode?: "viewport" | "fullPage", width?: number, height?: number, delay?: number, waitFor?: string }`\n- Output: `{ screenshotUrl: string }`\n\n#### searchGmailEmails\nSearch for emails in the connected Gmail account using a Gmail search query. To list recent inbox emails, pass an empty query string.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Uses Gmail search syntax (e.g. "from:user@example.com", "subject:invoice", "is:unread").\n- To list recent inbox emails, use an empty query string or "in:inbox".\n- Returns up to 100 emails (default 5). The variable receives text or JSON depending on exportType.\n- The direct execution output always returns structured email objects.\n- Input: `{ query: string, connectionId?: string, exportType: "json" | "text", limit: string }`\n- Output: `{ emails: { id: string, subject: string, from: string, to: string, date: string, plainBody: string, htmlBody: string, labels: string }[] }`\n\n#### searchGoogle\nSearch the web using Google and return structured results.\n- Defaults to us/english, but can optionally specify country and/or language.\n- Defaults to any time, but can optionally specify last hour, last day, week, month, or year.\n- Defaults to top 30 results, but can specify 1 to 100 results to return.\n- Set fetchTopN to also read the page content of the first N results in the same step, instead of following up with a separate Scrape URL step per result. Costs meaningfully more (each page is a full scrape), so ask for the fewest you need.\n- Also returns Google\'s "people also ask" and related searches when available, which are useful for choosing a follow-up query.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, languageCode?: string, dateRange?: "hour" | "day" | "week" | "month" | "year" | "any", numResults?: number, fetchTopN?: number }`\n- Output: `{ results: { title: string, description: string, url: string, content?: string }[], peopleAlsoAsk?: { question?: string, snippet?: string, link?: string }[], relatedSearches?: string[] }`\n\n#### searchGoogleImages\nSearch Google Images and return image results with URLs and metadata.\n- Defaults to us/english, but can optionally specify country and/or language.\n- Defaults to any time, but can optionally specify last hour, last day, week, month, or year.\n- Defaults to top 30 results, but can specify 1 to 100 results to return.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, languageCode?: string, dateRange?: "hour" | "day" | "week" | "month" | "year" | "any", numResults?: number }`\n- Output: `{ images: { title: string, imageUrl: string, imageWidth: number, imageHeight: number, thumbnailUrl: string, thumbnailWidth: number, thumbnailHeight: number, source: string, domain: string, link: string, googleUrl: string, position: number }[] }`\n\n#### searchGoogleNews\nSearch Google News for recent news articles matching a query.\n- Defaults to top 30 results, but can specify 1 to 100 results to return.\n- Input: `{ text: string, exportType: "text" | "json", numResults?: number }`\n- Output: `{ articles: { title: string, link: string, date: string, source: { name: string }, snippet?: string }[] }`\n\n#### searchGoogleTrends\nFetch Google Trends data for a search term.\n- date accepts shorthand ("now 1-H", "today 1-m", "today 5-y", etc.) or custom "yyyy-mm-dd yyyy-mm-dd" ranges.\n- data_type controls the shape of returned data: TIMESERIES, GEO_MAP, GEO_MAP_0, RELATED_TOPICS, or RELATED_QUERIES.\n- Input: `{ text: string, hl: string, geo: string, data_type: "TIMESERIES" | "GEO_MAP" | "GEO_MAP_0" | "RELATED_TOPICS" | "RELATED_QUERIES", cat: string, date: string, ts: string }`\n- Output: `{ trends: object }`\n\n#### searchPerplexity\nSearch the web using the Perplexity API and return structured results.\n- Defaults to US results. Use countryCode (ISO code) to filter by country.\n- Returns 10 results by default, configurable from 1 to 20.\n- The variable receives text or JSON depending on exportType. The direct execution output always returns structured results.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, numResults?: number }`\n- Output: `{ results: { title: string, description: string, url: string }[] }`\n\n#### sendEmail\nSend an email to one or more recipient addresses.\n- If you are writing a backend method for a v2 app, prefer mindstudio.email.send({...}) \u2014 it takes the same fields, returns the batchId you read stats back with, and sits alongside email.messages / email.batch / email.suppress. This action remains fully supported and runs the same code; it is also the only way to use connectionId or generateHtml.\n- Use the "to" field to send to one or more specific recipient email addresses directly. Allowed recipients depend on the sender: when the app sends from a domain it owns (a verified custom domain or its <slug>.madewithremy.com subdomain) any recipient is allowed; when it falls back to the shared Remy address, recipients must be verified app users or members of the app\'s organization. (v1 apps cannot use a direct "to" \u2014 they must resolve recipients via a connection.)\n- 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.\n- Use "cc" and "bcc" to add visible / hidden recipients (a string or an array). They are subject to the same recipient rules as "to".\n- "to" is optional only in the sense that recipients can come from elsewhere \u2014 omit it and supply "cc"/"bcc" for a hidden-list send, or omit all three and recipients are resolved from an OAuth connection. Naming nobody at all is an error.\n- Bcc-only sends are supported: with no "to" or "cc", the To: header is addressed to the app\'s own sender address (the standard "undisclosed recipients" pattern) so recipients can\'t see each other. The returned "recipients" reflects that auto-filled address.\n- Every recipient counts toward the app\'s daily outbound cap, including cc and bcc.\n- The sender defaults automatically: v2 apps send from the app\'s own identity \u2014 its verified custom domain if set, else its platform subdomain (noreply@<slug>.madewithremy.com), else the default Remy address.\n- Optionally set "from" to a custom handle, but ONLY if the app has a custom domain or subdomain: a bare handle ("support" \u2192 support@<app-domain>), a full "support@your-domain.com", or "Name <support@your-domain.com>". The domain must be one the app owns, or the step fails.\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- The body is interpreted automatically: if it already looks like HTML it is sent as HTML, otherwise it is rendered from Markdown. Every email is sent as multipart with a plain-text alternative auto-derived from the body (better deliverability).\n- Set bodyType to override interpretation: "html" (send as-is), "markdown" (render to HTML), or "text" (plain text only, no HTML part). Default is "auto".\n- Optionally set "text" to supply your own plain-text alternative instead of the auto-derived one.\n- Use "attachments" to attach files by URL. Each entry is a URL string, or an object { url, filename?, contentType? } to control the attachment\'s displayed filename and MIME type.\n- When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model (implies an HTML body).\n- Set replyTo to control the Reply-To address for replies.\n- For threaded replies in a shared inbox, set inReplyTo (the Message-ID being replied to) and references (prior Message-IDs in the thread).\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.\n- 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.\n- 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.\n- The subject is stored verbatim in the app\'s delivery log, which every workspace member can read for 90 days. Never put a secret in the subject \u2014 no one-time codes, no magic-link tokens, no password-reset tokens. Put those in the body of a "transactional" send: a small transactional send is delivered immediately and its body is never stored. A "marketing" send, or any large send, is queued for delivery, which means its body is held in internal storage until the send finishes \u2014 so do not put a secret in the body of one of those either.\n- Input: `{ subject: string, body: string, to?: string | string[], cc?: string | string[], bcc?: string | string[], connectionId?: string, generateHtml?: boolean, generateHtmlInstructions?: string, generateHtmlModelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, promptCaching?: boolean, config?: object }, attachments?: (string | { url: string, filename?: string, contentType?: string })[], from?: string, bodyType?: "auto" | "html" | "markdown" | "text", text?: string, replyTo?: string, inReplyTo?: string, references?: string[], category?: "transactional" | "marketing", batchId?: string }`\n- Output: `{ recipients: string[], cc: string[], bcc: string[], from: string, suppressed: string[], batchId: string }`\n\n#### sendGmailDraft\nSend an existing draft from the connected Gmail account.\n- Requires a Google OAuth connection with Gmail compose scope.\n- The draft is sent and removed from the Drafts folder.\n- Use the draft ID returned by the Create Gmail Draft or List Gmail Drafts steps.\n- Input: `{ draftId: string, connectionId?: string }`\n- Output: `{ messageId: string }`\n\n#### sendGmailMessage\nSend an email from the connected Gmail account.\n- Requires a Google OAuth connection with Gmail compose scope.\n- messageType controls the body format: "plain" for plain text, "html" for raw HTML, "markdown" for auto-converted markdown.\n- Input: `{ to: string, subject: string, message: string, connectionId?: string, messageType: "plain" | "html" | "markdown" }`\n- Output: `{ messageId: string }`\n\n#### sendSlackDirectMessage\nSend a direct message to a Slack user via a connected bot.\n- The user is responsible for connecting their Slack workspace\n- The recipient is identified by their Slack user ID\n- Supports both simple text messages and Slack blocks messages\n- Text messages can use limited markdown (slack-only formatting\u2014e.g., headers are just rendered as bold)\n- Input: `{ slackUserId: string, messageType: "string" | "blocks", message: string, connectionId?: string }`\n- Output: `unknown`\n\n#### sendSMS\nSend an SMS or MMS message to a phone number configured via OAuth connection.\n- User is responsible for configuring the connection to the number (MindStudio requires double opt-in to prevent spam)\n- If mediaUrls are provided, the message is sent as MMS instead of SMS\n- MMS supports up to 10 media URLs (images, video, audio, PDF) with a 5MB limit per file\n- MMS is only supported on US and Canadian carriers; international numbers will receive SMS only (media silently dropped)\n- Input: `{ body: string, connectionId?: string, mediaUrls?: string[] }`\n- Output: `unknown`\n\n#### setGmailReadStatus\nMark one or more Gmail emails as read or unread.\n- Requires a Google OAuth connection with Gmail modify scope.\n- Accepts one or more message IDs as a comma-separated string or array.\n- Set markAsRead to true to mark as read, false to mark as unread.\n- Input: `{ messageIds: string, markAsRead: boolean, connectionId?: string }`\n- Output: `{ messageIds: string[] }`\n\n#### setRunTitle\nSet the title of the agent run for the user\'s history\n- Input: `{ title: string }`\n- Output: `unknown`\n\n#### setVariable\nExplicitly set a variable to a given value.\n- Useful for bootstrapping global variables or setting constants.\n- The variable name and value both support variable interpolation.\n- The type field is a UI hint only (controls input widget in the editor).\n- Input: `{ value: string | string[] }`\n- Output: `object`\n\n#### textToSpeech\nGenerate an audio file from provided text using a speech model.\n- The text field contains the exact words to be spoken (not instructions).\n- Voice, delivery and format are set through `speechModelOverride.config`, whose keys are the model\'s own (look a model up with its id). Delivery direction, where a model takes one, is never spoken: `styleInstruction` on gemini-3.8-flash-tts and gemini-3.8-flash-lite-tts, `instructions` on gpt-realtime-2.1-mini-tts, `voice_instruction` on seed-speech-tts-2.0, `emotion` on cartesia-sonic-3.6 and the MiniMax models.\n- Tags in the text act at that point, in each vendor\'s own syntax: `[laughs]` or `[whispers]` on elevenlabs-tts with Eleven v4 or v3; `<laugh>`, `<sigh>` or `<whispers>` on the Gemini 3.8 models; `[laugh]`, `[pause]` or `<whisper>\u2026</whisper>` on grok-voice-tts; `[laughter]` or `<emotion value="excited"/>` on cartesia-sonic-3.6; `[be happy and upbeat]` on inworld-tts-2.\n- A dialogue between voices: set `$mode: \'dialogue\'` and name each speaker (`speaker1_name`, `speaker1_voice`, `speaker2_name`, \u2026), then write the text as a script, one `Name: line` per turn. Supported by elevenlabs-tts (Eleven v4 or v3, up to 4 speakers) and the Gemini 3.8 models (exactly 2).\n- A text longer than a model takes in one request is spoken in pieces and joined, and then comes back as MP3 whatever format was asked for.\n- In foreground mode, the audio is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ text: string, intermediateAsset?: boolean, speechModelOverride?: { model: string, config?: object } }`\n- Output: `{ audioUrl: string }`\n\n#### transcribeAudio\nConvert an audio file to text using a transcription model.\n- The default model is elevenlabs-scribe-v2: speakers, word timings, key terms and redaction, at any length.\n- The prompt field gives context (speaker names, domain) to OpenAI\'s and DeepInfra\'s models. Other models ignore it; give them `keyterms` instead.\n- Options go in `transcriptionModelOverride.config`, and support varies by model:\n- `language` \u2014 the spoken language as a code such as `en` or `es`; left out, the model detects it. Every model takes it. deepgram-nova-3 assumes `en` unless told, and also takes `multi` for speech that switches language (billed at its multilingual rate).\n- `detectLanguage: \'yes\'` \u2014 detect the language instead of assuming English (deepgram-nova-3 only).\n- `includeSpeakers: \'yes\'` \u2014 label each passage with a speaker. Supported by elevenlabs-scribe-v2, elevenlabs-scribe-v2-medical, deepgram-nova-3, grok-stt, voxtral-mini-transcribe-2 and gemini-3.5-transcribe; gpt-4o-transcribe-diarize always does. Scribe and Voxtral also switch `text` to a timestamped, speaker-labelled block format; Deepgram, Gemini and gpt-4o-transcribe-diarize to `Speaker N:` lines.\n- `numSpeakers` \u2014 the most people talking, to help tell them apart (Scribe).\n- `keyterms` \u2014 a list of names, products and terms the audio contains, so they\'re spelled right. Supported by gpt-transcribe, Scribe, deepgram-nova-3, grok-stt, voxtral-mini-transcribe-2 and gemini-3.5-transcribe. Billed as an add-on on Scribe and Deepgram.\n- `redactPii: \'yes\'` \u2014 replace names, phone numbers, addresses and other personal information with a label of what was there (Scribe, deepgram-nova-3). Billed as an add-on.\n- `includeWordTimings: \'yes\'` \u2014 add per-word timings and confidence inside each segment. Supported by Scribe, deepgram-nova-3, whisper-1, grok-stt, voxtral-mini-transcribe-2 and gemini-3.5-transcribe. Off by default because an hour of speech is roughly ten thousand words.\n- `subtitles: \'srt\' | \'vtt\'` \u2014 also return the transcript as a subtitle file, in the `subtitles` output, timed by the segments. Any model that returns segments.\n- gemini-3.5-transcribe can\'t take `keyterms` together with speakers, word timings or subtitles, and voxtral-mini-transcribe-2 can\'t take `language` together with word timings or subtitles; either asked for both is refused.\n- The `segments` output carries timings, confidence and speakers as structured data, so there is no need to slice the audio up to recover them. It is populated by Scribe, deepgram-nova-3, grok-stt, whisper-1, gpt-4o-transcribe-diarize, voxtral-mini-transcribe-2 (unless given a language), gemini-3.5-transcribe (with word timings or subtitles) and the DeepInfra-served whisper and qwen models, and absent for models that return nothing but text (gpt-transcribe, gpt-4o-transcribe, gpt-4o-mini-transcribe, the DeepInfra-served voxtral models).\n- In a workflow, `destinationVar` receives the transcript text. Name `segmentsVar` to also keep the `segments` array. A model that returns no segments leaves that variable unset.\n- A recording of any length can be transcribed. One longer than a model takes at once (OpenAI\'s and DeepInfra\'s models, Gemini) is transcribed in pieces and joined, its timings running from the start of the recording. Speakers can\'t be followed from one piece to the next, so with speakers on, such a recording is refused; Scribe and Deepgram label speakers at any length.\n- `confidence` is always 0..1 with higher meaning more confident, whichever model produced it.\n- Input: `{ audioUrl: string, prompt: string, transcriptionModelOverride?: { model: string, config?: object } }`\n- Output: `{ text: string, segments?: { start: number, end: number, text: string, confidence?: number, speaker?: string, language?: string, words?: { text: string, start?: number, end?: number, confidence?: number, speaker?: string }[] }[], language?: string, durationSeconds?: number, subtitles?: string }`\n\n#### trimMedia\nTrim an audio or video clip\n- Input: `{ inputUrl: string, start?: number | string, duration?: string | number, intermediateAsset?: boolean }`\n- Output: `{ mediaUrl: string }`\n\n#### updateGmailLabels\nAdd or remove labels on Gmail messages, identified by message IDs or a search query.\n- Requires a Google OAuth connection with Gmail modify scope.\n- Provide either a query (Gmail search syntax) or explicit messageIds to target messages.\n- Label IDs can be label names or Gmail label IDs \u2014 names are resolved automatically.\n- Input: `{ query: string, connectionId?: string, messageIds: string, addLabelIds: string, removeLabelIds: string }`\n- Output: `{ updatedMessageIds: string[] }`\n\n#### uploadDataSourceDocument\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Upload a file into an existing data source from a URL or raw text content.\n- If "file" is a single URL, the file is downloaded from that URL and uploaded.\n- If "file" is any other string, a .txt document is created from that content and uploaded.\n- The block waits (polls) for processing to complete before transitioning, up to 5 minutes.\n- Once processing finishes, vectors are loaded into Milvus so the data source is immediately queryable.\n- Supported file types (when using a URL) are the same as the data source upload UI (PDF, DOCX, TXT, etc.).\n- Input: `{ dataSourceId: string, file: string, fileName: string }`\n- Output: `{ documentId: string, dataSourceId: string, fileName: string }`\n\n#### upscaleImage\nIncrease the resolution of an image using AI upscaling.\n- Output is a PNG on the public CDN, or in one of the app\'s own file stores when the call passes `store` \u2014 a private store keeps it private.\n- Input: `{ imageUrl: string, targetResolution: "2k" | "4k" | "8k", engine: "standard" | "pro" }`\n- Output: `{ imageUrl: string }`\n\n#### upscaleVideo\nUpscale a video file\n- Input: `{ videoUrl: string, targetResolution: "720p" | "1080p" | "2K" | "4K", engine: "standard" | "pro" | "ultimate" | "flashvsr" | "seedance" | "seedvr2" | "runwayml/upscale-v1", intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### videoFaceSwap\nSwap faces in a video file\n- Input: `{ videoUrl: string, faceImageUrl: string, targetIndex: number, engine: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### videoRemoveBackground\nRemove or replace background from a video\n- Input: `{ videoUrl: string, newBackground: "transparent" | "image", newBackgroundImageUrl?: string, engine: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### videoRemoveWatermark\nRemove a watermark from a video\n- Input: `{ videoUrl: string, engine: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### watermarkImage\nOverlay a watermark image onto another image.\n- The watermark is placed at the specified corner with configurable padding and width.\n- Input: `{ imageUrl: string, watermarkImageUrl: string, corner: "top-left" | "top-right" | "bottom-left" | "bottom-right", paddingPx: number, widthPx: number, intermediateAsset?: boolean }`\n- Output: `{ imageUrl: string }`\n\n#### watermarkVideo\nAdd an image watermark to a video\n- Input: `{ videoUrl: string, imageUrl: string, corner: "top-left" | "top-right" | "bottom-left" | "bottom-right", paddingPx: number, widthPx: number, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n### ActiveCampaign\n\n#### activeCampaignAddNote\nAdd a note to an existing contact in ActiveCampaign.\n- Requires an ActiveCampaign OAuth connection (connectionId).\n- The contact must already exist \u2014 use the contact ID from a previous create or search step.\n- Input: `{ contactId: string, note: string, connectionId?: string }`\n- Output: `unknown`\n\n#### activeCampaignCreateContact\nCreate or sync a contact in ActiveCampaign.\n- Requires an ActiveCampaign OAuth connection (connectionId).\n- If a contact with the email already exists, it may be updated depending on ActiveCampaign settings.\n- Custom fields are passed as a key-value map where keys are field IDs.\n- Input: `{ email: string, firstName: string, lastName: string, phone: string, accountId: string, customFields: object, connectionId?: string }`\n- Output: `{ contactId: string }`\n\n### Airtable\n\n#### airtableCreateUpdateRecord\nCreate a new record or update an existing record in an Airtable table.\n- If recordId is provided, updates that record. Otherwise, creates a new one.\n- When updating with updateMode "onlySpecified", unspecified fields are left as-is. With "all", unspecified fields are cleared.\n- Array fields (e.g. multipleAttachments) accept arrays of values.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, recordId?: string, updateMode?: "onlySpecified" | "all", fields: unknown, recordData: object }`\n- Output: `{ recordId: string }`\n\n#### airtableDeleteRecord\nDelete a record from an Airtable table by its record ID.\n- Requires an active Airtable OAuth connection (connectionId).\n- Silently succeeds if the record does not exist.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, recordId: string }`\n- Output: `{ deleted: boolean }`\n\n#### airtableGetRecord\nFetch a single record from an Airtable table by its record ID.\n- Requires an active Airtable OAuth connection (connectionId).\n- If the record is not found, returns a string message instead of a record object.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, recordId: string }`\n- Output: `{ record: { id: string, createdTime: string, fields: object } | null }`\n\n#### airtableGetTableRecords\nFetch multiple records from an Airtable table with optional pagination.\n- Requires an active Airtable OAuth connection (connectionId).\n- Default limit is 100 records. Maximum is 1000.\n- When outputFormat is \'csv\', the variable receives CSV text. The direct execution output always returns parsed records.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, outputFormat?: "json" | "csv", limit?: number }`\n- Output: `{ records: { id: string, createdTime: string, fields: object }[] }`\n\n### Apollo\n\n#### enrichPerson\nLook up professional information about a person using Apollo.io. Search by ID, name, LinkedIn URL, email, or domain.\n- At least one search parameter must be provided.\n- Returns enriched data from Apollo including contact details, employment info, and social profiles.\n- Input: `{ params: { id: string, name: string, linkedinUrl: string, email: string, domain: string } }`\n- Output: `{ data: unknown }`\n\n#### peopleSearch\nSearch for people matching specific criteria using Apollo.io. Supports natural language queries and advanced filters.\n- Can use a natural language "smartQuery" which is converted to Apollo search parameters by an AI model.\n- Advanced params can override or supplement the smart query results.\n- Optionally enriches returned people and/or their organizations for additional detail.\n- Results are paginated. Use limit and page to control the result window.\n- Input: `{ smartQuery: string, enrichPeople: boolean, enrichOrganizations: boolean, limit: string, page: string, params: { personTitles: string, includeSimilarTitles: string, qKeywords: string, personLocations: string, personSeniorities: string, organizationLocations: string, qOrganizationDomainsList: string, contactEmailStatus: string, organizationNumEmployeesRanges: string, revenueRangeMin: string, revenueRangeMax: string, currentlyUsingAllOfTechnologyUids: string, currentlyUsingAnyOfTechnologyUids: string, currentlyNotUsingAnyOfTechnologyUids: string } }`\n- Output: `{ results: unknown }`\n\n### Coda\n\n#### codaCreateUpdatePage\nCreate a new page or update an existing page in a Coda document.\n- Requires a Coda OAuth connection (connectionId).\n- If pageData.pageId is provided, updates that page. Otherwise, creates a new one.\n- Page content is provided as markdown and converted to Coda\'s canvas format.\n- When updating, insertionMode controls how content is applied (default: \'append\').\n- Input: `{ connectionId?: string, pageData: { docId: string, pageId?: string, name: string, subtitle: string, iconName: string, imageUrl: string, parentPageId?: string, pageContent: string | unknown, contentUpdate?: unknown, insertionMode?: string } }`\n- Output: `{ pageId: string }`\n\n#### codaCreateUpdateRow\nCreate a new row or update an existing row in a Coda table.\n- Requires a Coda OAuth connection (connectionId).\n- If rowId is provided, updates that row. Otherwise, creates a new one.\n- Row data keys are column IDs. Empty values are excluded.\n- Input: `{ connectionId?: string, docId: string, tableId: string, rowId?: string, rowData: object }`\n- Output: `{ rowId: string }`\n\n#### codaFindRow\nSearch for a row in a Coda table by matching column values.\n- Requires a Coda OAuth connection (connectionId).\n- Returns the first row matching all specified column values, or null if no match.\n- Search criteria in rowData are ANDed together.\n- Input: `{ connectionId?: string, docId: string, tableId: string, rowData: object }`\n- Output: `{ row: { id: string, values: object } | null }`\n\n#### codaGetPage\nExport and read the contents of a page from a Coda document.\n- Requires a Coda OAuth connection (connectionId).\n- Page export is asynchronous on Coda\'s side \u2014 there may be a brief delay while it processes.\n- If a page was just created in a prior step, there is an automatic 20-second retry if the first export attempt fails.\n- Input: `{ connectionId?: string, docId: string, pageId: string, outputFormat?: "html" | "markdown" }`\n- Output: `{ content: string }`\n\n#### codaGetTableRows\nFetch rows from a Coda table with optional pagination.\n- Requires a Coda OAuth connection (connectionId).\n- Default limit is 10000 rows. Rows are fetched in pages of 500.\n- When outputFormat is \'csv\', the variable receives CSV text. The direct execution output always returns parsed rows.\n- Input: `{ connectionId?: string, docId: string, tableId: string, limit?: number | string, outputFormat?: "json" | "csv" }`\n- Output: `{ rows: { id: string, values: object }[] }`\n\n### Gmail\n\n#### deleteGmailEmail\nMove an email to trash in the connected Gmail account (recoverable delete).\n- Requires a Google OAuth connection with Gmail modify scope.\n- Uses trash (recoverable) rather than permanent delete.\n- Input: `{ messageId: string, connectionId?: string }`\n- Output: `unknown`\n\n#### getGmailDraft\nRetrieve a specific draft from Gmail by draft ID.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns the draft content including subject, recipients, sender, and body.\n- Input: `{ draftId: string, connectionId?: string }`\n- Output: `{ draftId: string, messageId: string, subject: string, to: string, from: string, body: string }`\n\n#### getGmailEmail\nRetrieve a specific email from Gmail by message ID.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns the email subject, sender, recipient, date, body (plain text preferred, falls back to HTML), and labels.\n- Input: `{ messageId: string, connectionId?: string }`\n- Output: `{ messageId: string, subject: string, from: string, to: string, date: string, body: string, labels: string }`\n\n#### listGmailDrafts\nList drafts in the connected Gmail account.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns up to 50 drafts (default 10).\n- The variable receives text or JSON depending on exportType.\n- Input: `{ connectionId?: string, limit?: string, exportType: "json" | "text" }`\n- Output: `{ drafts: { draftId: string, messageId: string, subject: string, to: string, snippet: string }[] }`\n\n#### replyToGmailEmail\nReply to an existing email in Gmail. The reply is threaded under the original message.\n- Requires a Google OAuth connection with Gmail compose and readonly scopes.\n- The reply is sent to the original sender and threaded under the original message.\n- messageType controls the body format: "plain", "html", or "markdown".\n- Input: `{ messageId: string, message: string, messageType: "plain" | "html" | "markdown", connectionId?: string }`\n- Output: `{ messageId: string }`\n\n### Google\n\n#### createGoogleDoc\nCreate a new Google Document and optionally populate it with content.\n- textType determines how the text field is interpreted: "plain" for plain text, "html" for HTML markup, "markdown" for Markdown.\n- Input: `{ title: string, text: string, connectionId?: string, textType: "plain" | "html" | "markdown" }`\n- Output: `{ documentUrl: string }`\n\n#### createGoogleSheet\nCreate a new Google Spreadsheet and populate it with CSV data.\n- Input: `{ title: string, text: string, connectionId?: string }`\n- Output: `{ spreadsheetUrl: string }`\n\n#### deleteGoogleSheetRows\nDelete a range of rows from a Google Spreadsheet.\n- Requires a Google OAuth connection with Drive scope.\n- startRow and endRow are 1-based row numbers (inclusive).\n- If sheetName is omitted, operates on the first sheet.\n- Input: `{ documentId: string, sheetName?: string, startRow: string, endRow: string, connectionId?: string }`\n- Output: `unknown`\n\n#### fetchGoogleDoc\nFetch the contents of an existing Google Document.\n- exportType controls the output format: "html" for HTML markup, "markdown" for Markdown, "json" for structured JSON, "plain" for plain text.\n- Input: `{ documentId: string, connectionId?: string, exportType: "html" | "markdown" | "json" | "plain" }`\n- Output: `{ content: string }`\n\n#### fetchGoogleSheet\nFetch contents of a Google Spreadsheet range.\n- range uses A1 notation (e.g. "Sheet1!A1:C10"). Omit to fetch the entire first sheet.\n- exportType controls the output format: "csv" for comma-separated values, "json" for structured JSON.\n- Input: `{ spreadsheetId: string, range: string, connectionId?: string, exportType: "csv" | "json" }`\n- Output: `{ content: string }`\n\n#### getGoogleSheetInfo\nGet metadata about a Google Spreadsheet including sheet names, row counts, and column counts.\n- Requires a Google OAuth connection with Drive scope.\n- Returns the spreadsheet title and a list of all sheets with their dimensions.\n- Input: `{ documentId: string, connectionId?: string }`\n- Output: `{ title: string, sheets: { sheetId: number, title: string, rowCount: number, columnCount: number }[] }`\n\n#### updateGoogleDoc\nUpdate the contents of an existing Google Document.\n- operationType controls how content is applied: "addToTop" prepends, "addToBottom" appends, "overwrite" replaces all content.\n- textType determines how the text field is interpreted: "plain" for plain text, "html" for HTML markup, "markdown" for Markdown.\n- Input: `{ documentId: string, connectionId?: string, text: string, textType: "plain" | "html" | "markdown", operationType: "addToTop" | "addToBottom" | "overwrite" }`\n- Output: `{ documentUrl: string }`\n\n#### updateGoogleSheet\nUpdate a Google Spreadsheet with new data.\n- operationType controls how data is written: "addToBottom" appends rows, "overwrite" replaces all data, "range" writes to a specific cell range.\n- Data should be provided as CSV in the text field.\n- Input: `{ text: string, connectionId?: string, spreadsheetId: string, range: string, operationType: "addToBottom" | "overwrite" | "range" }`\n- Output: `{ spreadsheetUrl: string }`\n\n### Google Calendar\n\n#### createGoogleCalendarEvent\nCreate a new event on a Google Calendar.\n- Requires a Google OAuth connection with Calendar events scope.\n- Date/time values must be ISO 8601 format (e.g. "2025-07-02T10:00:00-07:00").\n- Attendees are specified as one email address per line in a single string.\n- Set addMeetLink to true to automatically attach a Google Meet video call.\n- Input: `{ connectionId?: string, summary: string, description?: string, location?: string, startDateTime: string, endDateTime: string, attendees?: string, addMeetLink?: boolean, calendarId?: string }`\n- Output: `{ eventId: string, htmlLink: string }`\n\n#### deleteGoogleCalendarEvent\nDelete an event from a Google Calendar by event ID.\n- Requires a Google OAuth connection with Calendar events scope.\n- Throws an error if the event does not exist.\n- Input: `{ connectionId?: string, eventId: string, calendarId?: string }`\n- Output: `unknown`\n\n#### getGoogleCalendarEvent\nRetrieve a specific event from a Google Calendar by event ID.\n- Requires a Google OAuth connection with Calendar events scope.\n- The variable receives JSON or XML-like text depending on exportType. The direct execution output always returns the structured event.\n- Input: `{ connectionId?: string, eventId: string, exportType: "json" | "text", calendarId?: string }`\n- Output: `{ event: { id?: string | null, status?: string | null, htmlLink?: string | null, created?: string | null, updated?: string | null, summary?: string | null, description?: string | null, location?: string | null, organizer?: { displayName?: string | null, email?: string | null } | null, start?: { dateTime?: string | null, timeZone?: string | null } | null, end?: { dateTime?: string | null, timeZone?: string | null } | null, attendees?: ({ displayName?: string | null, email?: string | null, responseStatus?: string | null })[] | null } }`\n\n#### listGoogleCalendarEvents\nList upcoming events from a Google Calendar, ordered by start time.\n- Requires a Google OAuth connection with Calendar events scope.\n- Only returns future events (timeMin = now).\n- The variable receives JSON or XML-like text depending on exportType. The direct execution output always returns structured events.\n- Input: `{ connectionId?: string, limit: number, exportType: "json" | "text", calendarId?: string }`\n- Output: `{ events: ({ id?: string | null, status?: string | null, htmlLink?: string | null, created?: string | null, updated?: string | null, summary?: string | null, description?: string | null, location?: string | null, organizer?: { displayName?: string | null, email?: string | null } | null, start?: { dateTime?: string | null, timeZone?: string | null } | null, end?: { dateTime?: string | null, timeZone?: string | null } | null, attendees?: ({ displayName?: string | null, email?: string | null, responseStatus?: string | null })[] | null })[] }`\n\n#### searchGoogleCalendarEvents\nSearch for events in a Google Calendar by keyword, date range, or both.\n- Requires a Google OAuth connection with Calendar events scope.\n- Supports keyword search via "query" and date filtering via "timeMin"/"timeMax" (ISO 8601 format).\n- Unlike "List Events" which only shows future events, this allows searching past events too.\n- Input: `{ query?: string, timeMin?: string, timeMax?: string, calendarId?: string, limit?: number, exportType: "json" | "text", connectionId?: string }`\n- Output: `{ events: ({ id?: string | null, status?: string | null, htmlLink?: string | null, created?: string | null, updated?: string | null, summary?: string | null, description?: string | null, location?: string | null, organizer?: { displayName?: string | null, email?: string | null } | null, start?: { dateTime?: string | null, timeZone?: string | null } | null, end?: { dateTime?: string | null, timeZone?: string | null } | null, attendees?: ({ displayName?: string | null, email?: string | null, responseStatus?: string | null })[] | null })[] }`\n\n#### updateGoogleCalendarEvent\nUpdate an existing event on a Google Calendar. Only specified fields are changed.\n- Requires a Google OAuth connection with Calendar events scope.\n- Fetches the existing event first, then applies only the provided updates. Omitted fields are left unchanged.\n- Attendees are specified as one email address per line, and replace the entire attendee list.\n- Input: `{ connectionId?: string, eventId: string, summary?: string, description?: string, location?: string, startDateTime?: string, endDateTime?: string, attendees?: string, calendarId?: string }`\n- Output: `{ eventId: string, htmlLink: string }`\n\n### Google Drive\n\n#### getGoogleDriveFile\nDownload a file from Google Drive and rehost it on the CDN. Returns a public CDN URL.\n- Requires a Google OAuth connection with Drive scope.\n- Google-native files (Docs, Sheets, Slides) cannot be downloaded \u2014 use dedicated steps instead.\n- Maximum file size: 200MB.\n- The file is downloaded and stored on the public CDN, where the returned URL is publicly accessible \u2014 pass `store` to put it in one of the app\'s own file stores instead, which for a private store means the URL only loads for an authorized session.\n- Input: `{ fileId: string, connectionId?: string }`\n- Output: `{ url: string, name: string, mimeType: string, size: number }`\n\n#### listGoogleDriveFiles\nList files in a Google Drive folder.\n- Requires a Google OAuth connection with Drive scope.\n- If folderId is omitted, lists files in the root folder.\n- Returns file metadata including name, type, size, and links.\n- Input: `{ folderId?: string, limit?: number, connectionId?: string, exportType: "json" | "text" }`\n- Output: `{ files: { id: string, name: string, mimeType: string, size: string, webViewLink: string, createdTime: string, modifiedTime: string }[] }`\n\n#### searchGoogleDrive\nSearch for files in Google Drive by keyword.\n- Requires a Google OAuth connection with Drive scope.\n- Searches file content and names using Google Drive\'s fullText search.\n- Input: `{ query: string, limit?: number, connectionId?: string, exportType: "json" | "text" }`\n- Output: `{ files: { id: string, name: string, mimeType: string, size: string, webViewLink: string, createdTime: string, modifiedTime: string }[] }`\n\n### HubSpot\n\n#### hubspotCreateCompany\nCreate a new company or update an existing one in HubSpot. Matches by domain.\n- Requires a HubSpot OAuth connection (connectionId).\n- If a company with the given domain already exists, it is updated. Otherwise, a new one is created.\n- Property values are type-checked against enabledProperties before being sent to HubSpot.\n- Input: `{ connectionId?: string, company: { domain: string, name: string }, enabledProperties: ({ label: string, value: string, type: "string" | "number" | "bool" })[] }`\n- Output: `{ companyId: string }`\n\n#### hubspotCreateContact\nCreate a new contact or update an existing one in HubSpot. Matches by email address.\n- Requires a HubSpot OAuth connection (connectionId).\n- If a contact with the given email already exists, it is updated. Otherwise, a new one is created.\n- If companyDomain is provided, the contact is associated with that company (creating the company if needed).\n- Property values are type-checked against enabledProperties before being sent to HubSpot.\n- Input: `{ connectionId?: string, contact: { email: string, firstname: string, lastname: string }, enabledProperties: ({ label: string, value: string, type: "string" | "number" | "bool" })[], companyDomain: string }`\n- Output: `{ contactId: string }`\n\n#### hubspotGetCompany\nLook up a HubSpot company by domain name or company ID.\n- Requires a HubSpot OAuth connection (connectionId).\n- Returns null if the company is not found.\n- When searching by domain, performs a search query then fetches the full company record.\n- Use additionalProperties to request specific HubSpot properties beyond the defaults.\n- Input: `{ connectionId?: string, searchBy: "domain" | "id", companyDomain: string, companyId: string, additionalProperties: string[] }`\n- Output: `{ company: { id: string, properties: object, createdAt: string, updatedAt: string, archived: boolean } | null }`\n\n#### hubspotGetContact\nLook up a HubSpot contact by email address or contact ID.\n- Requires a HubSpot OAuth connection (connectionId).\n- Returns null if the contact is not found.\n- Use additionalProperties to request specific HubSpot properties beyond the defaults.\n- Input: `{ connectionId?: string, searchBy: "email" | "id", contactEmail: string, contactId: string, additionalProperties: string[] }`\n- Output: `{ contact: { id: string, properties: object, createdAt: string, updatedAt: string, archived: boolean } | null }`\n\n### Hunter.io\n\n#### hunterApiCompanyEnrichment\nLook up company information by domain using Hunter.io.\n- Returns company name, description, location, industry, size, technologies, and more.\n- If the domain input is a full URL, the hostname is automatically extracted.\n- Returns null if the company is not found.\n- Input: `{ domain: string }`\n- Output: `{ data: { name: string, domain: string, description: string | null, country: string | null, state: string | null, city: string | null, industry: string | null, employees_range: string | null, logo_url: string | null, technologies: string[] } | null }`\n\n#### hunterApiDomainSearch\nSearch for email addresses associated with a domain using Hunter.io.\n- If the domain input is a full URL, the hostname is automatically extracted.\n- Returns a list of email addresses found for the domain along with organization info.\n- Input: `{ domain: string }`\n- Output: `{ data: { domain: string, disposable: boolean, webmail: boolean, accept_all: boolean, pattern: string, organization: string, country: string | null, state: string | null, emails: ({ value: string, type: string, confidence: number, first_name: string | null, last_name: string | null, position: string | null, seniority: string | null, department: string | null, linkedin: string | null, twitter: string | null, phone_number: string | null })[], linked_domains: string[] } }`\n\n#### hunterApiEmailFinder\nFind an email address for a specific person at a domain using Hunter.io.\n- Requires a first name, last name, and domain.\n- If the domain input is a full URL, the hostname is automatically extracted.\n- Returns the most likely email address with a confidence score.\n- Input: `{ domain: string, firstName: string, lastName: string }`\n- Output: `{ data: { first_name: string, last_name: string, email: string, score: number, domain: string, accept_all: boolean, position: string | null, twitter: string | null, linkedin_url: string | null, phone_number: string | null, company: string | null, sources: { domain: string, uri: string, extracted_on: string }[] } }`\n\n#### hunterApiEmailVerification\nVerify whether an email address is valid and deliverable using Hunter.io.\n- Checks email format, MX records, SMTP server, and mailbox deliverability.\n- Returns a status ("valid", "invalid", "accept_all", "webmail", "disposable", "unknown") and a score.\n- Input: `{ email: string }`\n- Output: `{ data: { status: string, result: string, score: number, email: string, regexp: boolean, gibberish: boolean, disposable: boolean, webmail: boolean, mx_records: boolean, smtp_server: boolean, smtp_check: boolean, accept_all: boolean, block: boolean, sources: { domain: string, uri: string, extracted_on: string }[] } }`\n\n#### hunterApiPersonEnrichment\nLook up professional information about a person by their email address using Hunter.io.\n- Returns name, job title, social profiles, and company information.\n- If the person is not found, returns an object with an error message instead of throwing.\n- Input: `{ email: string }`\n- Output: `{ data: { first_name: string, last_name: string, email: string, position: string | null, seniority: string | null, department: string | null, linkedin_url: string | null, twitter: string | null, phone_number: string | null, company: { name: string, domain: string, industry: string | null } | null } | { error: string } }`\n\n### LinkedIn\n\n#### postToLinkedIn\nCreate a post on LinkedIn from the connected account.\n- Requires a LinkedIn OAuth connection (connectionId).\n- Supports text posts, image posts, video posts, document posts, and article posts.\n- Attach one media type per post: image, video, document, or article.\n- Documents support PDF, PPT, PPTX, DOC, DOCX (max 100MB, 300 pages). Displays as a slideshow carousel.\n- Articles create a link preview with optional custom title, description, and thumbnail.\n- Visibility controls who can see the post.\n- Input: `{ message: string, visibility: "PUBLIC" | "CONNECTIONS", imageUrl?: string, videoUrl?: string, documentUrl?: string, articleUrl?: string, titleText?: string, descriptionText?: string, connectionId?: string }`\n- Output: `unknown`\n\n### Meshy\n\n#### meshyAnimate\nApply a preset animation to a rigged 3D character model using Meshy.\n- Requires a rig_task_id from a previously completed Meshy rigging step.\n- Select an animation from Meshy\'s library of 600+ preset animations.\n- Only works with humanoid (bipedal) rigged characters.\n- Supports post-processing: FPS change (24/25/30/60), FBX-to-USDZ conversion, or armature extraction.\n- Animation categories: DailyActions, WalkAndRun, Fighting, Dancing, BodyMovements.\n- Input: `{ rigTaskId: string, actionId: number }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyImageTo3d\nGenerate a 3D model from one or more images using Meshy. Uses the multi-image-to-3D endpoint.\n- Accepts 1-4 image URLs. All images should depict the same object from different angles for best results.\n- By default generates with textures. Set shouldTexture to false for mesh-only output.\n- Uses should_remesh: false to preserve UV mapping integrity.\n- Input: `{ imageUrls: string[], shouldTexture?: boolean, topology?: string, targetPolycount?: number, symmetryMode?: string, poseMode?: string, textureImageUrl?: string }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyRemesh\nRemesh an existing 3D model to adjust topology, polygon count, or convert formats using Meshy.\n- Provide either an input task ID (from a previous Meshy step) or a model URL.\n- Defaults to triangle topology with 30,000 target polys.\n- Useful for reducing face count before rigging (max 300k faces for rigging).\n- Input: `{ inputTaskId?: string, modelUrl?: string, topology?: string, targetPolycount?: number, resizeHeight?: number }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyRig\nAuto-rig a humanoid 3D model and generate basic walking/running animations using Meshy.\n- Only works well with standard humanoid (bipedal) models with clearly defined limbs.\n- Prefers model_url over input_task_id for cleaner rigging input.\n- Models with more than 300,000 faces should be remeshed first.\n- Returns rigged model files and optional basic animations.\n- Input: `{ inputTaskId?: string, modelUrl?: string, heightMeters?: number }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyTextTo3d\nGenerate a 3D model preview from a text prompt using Meshy. Produces an untextured mesh (preview stage).\n- Creates a text-to-3D preview task (mesh generation only, no texture).\n- Use the Meshy Texture step to apply textures to the preview.\n- Maximum prompt length is 600 characters.\n- Input: `{ prompt: string, modelType?: string, topology?: string, targetPolycount?: number, symmetryMode?: string, poseMode?: string }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyTexture\nApply or replace textures on a 3D model using a text prompt or reference image via Meshy.\n- Provide either an input task ID (from a previous Meshy step) or a model URL.\n- Provide either a text style prompt or an image style URL to guide texturing.\n- Supports .glb, .gltf, .obj, .fbx, .stl model formats when using modelUrl.\n- By default preserves original UVs (enableOriginalUv = true).\n- Works with any model source: text-to-3D previews, image-to-3D, remeshed models, or external files.\n- Input: `{ inputTaskId?: string, modelUrl?: string, textStylePrompt?: string, imageStyleUrl?: string, enableOriginalUv?: boolean, enablePbr?: boolean }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n### Notion\n\n#### notionCreatePage\nCreate a new page in Notion as a child of an existing page.\n- Requires a Notion OAuth connection (connectionId).\n- Content is provided as markdown and converted to Notion blocks (headings, paragraphs, lists, code, quotes).\n- The page is created as a child of the specified parent page (pageId).\n- Input: `{ pageId: string, content: string, title: string, connectionId?: string }`\n- Output: `{ pageId: string, pageUrl: string }`\n\n#### notionUpdatePage\nUpdate the content of an existing Notion page.\n- Requires a Notion OAuth connection (connectionId).\n- Content is provided as markdown and converted to Notion blocks.\n- "append" mode adds content to the end of the page. "overwrite" mode deletes all existing blocks first.\n- Input: `{ pageId: string, content: string, mode: "append" | "overwrite", connectionId?: string }`\n- Output: `{ pageId: string, pageUrl: string }`\n\n### QuickBooks\n\n#### quickbooksApiRequest\nCall any QuickBooks Online Accounting API route using a connected company.\n- Requires a QuickBooks OAuth connection (connectionId).\n- Route is relative to /v3/company/{realmId}/, e.g. "account", "account/93", "query", "invoice".\n- Use queryParams for query-string values such as query=select * from Account.\n- Body is JSON for POST, PUT, and PATCH.\n- Input: `{ connectionId?: string, method: string, route: string, queryParams: object, body: string }`\n- Output: `{ ok: boolean, status: number, body: unknown }`\n\n### TypeSafe\n\n#### typesafeEvaluate\nUse evaluateDecision: the same questions, answers and models, with decisionModelOverride in place of model. evaluateDecision\'s earlier name, which still runs: ask structured questions about text, JSON, or images and get calibrated answers from a decision model.\n- Runs exactly as evaluateDecision does, with the same questions, answers, models and limits (see its notes), and model in place of decisionModelOverride.model.\n- Input: `{ state: string | object | unknown[], questions: object, model?: string, images?: string[] }`\n- Output: `{ model: string, answers: object, usage: { input_tokens: number, output_tokens: number } }`\n\n### X\n\n#### postToX\nCreate a post on X (Twitter) from the connected account.\n- Requires an X OAuth connection (connectionId).\n- Maximum 280 characters of text.\n- Optionally attach up to 4 media items (images, GIFs, or videos) via mediaUrls.\n- Media URLs must be publicly accessible. The service fetches and uploads them to X.\n- Supported formats: JPEG, PNG, GIF, WEBP, MP4. Images up to 5MB, videos up to 512MB.\n- Input: `{ text: string, connectionId?: string, mediaUrls?: string[] }`\n- Output: `unknown`\n\n#### searchXPosts\nSearch recent X (Twitter) posts matching a query.\n- Searches only the past 7 days of posts.\n- Query supports X API v2 search operators (up to 512 characters).\nAvailable search operators in query:\n| Operator | Description |\n| -----------------| -------------------------------------------------|\n| from: | Posts from a specific user (e.g., from:elonmusk) |\n| to: | Posts sent to a specific user (e.g., to:NASA) |\n| @ | Mentions a user (e.g., @openai) |\n| # | Hashtag search (e.g., #AI) |\n| is:retweet | Filters retweets |\n| is:reply | Filters replies |\n| has:media | Posts containing media (images, videos, or GIFs) |\n| has:links | Posts containing URLs |\n| lang: | Filters by language (e.g., lang:en) |\n| - | Excludes specific terms (e.g., -spam) |\n| () | Groups terms or operators (e.g., (AI OR ML)) |\n| AND, OR, NOT | Boolean logic for combining or excluding terms |\nConjunction-Required Operators (must be combined with a standalone operator):\n| Operator | Description |\n| ------------ | -----------------------------------------------|\n| has:media | Posts containing media (images, videos, or GIFs) |\n| has:links | Posts containing URLs |\n| is:retweet | Filters retweets |\n| is:reply | Filters replies |\nFor example, has:media alone is invalid, but #AI has:media is valid.\n- Input: `{ query: string, scope: "recent" | "all", options: { startTime?: string, endTime?: string, maxResults?: number } }`\n- Output: `{ posts: { id: string, authorId: string, dateCreated: string, text: string, stats: { retweets: number, replies: number, likes: number } }[] }`\n\n### You.com\n\n#### youDotComFinanceResearch\nAsk a financial research question using You.com Finance Research and return the sourced response.\n- Use this for financial questions such as company analysis, earnings, market research, filings, macroeconomics, and due diligence.\n- researchEffort supports deep (default) or exhaustive.\n- Finance Research returns the same response shape as Web Research, but searches a finance-optimized index.\n- Use it for cited synthesis, not raw price feeds or structured time-series exports.\n- Input: `{ input: string, researchEffort?: "deep" | "exhaustive" }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComGetPageContent\nFetch clean Markdown, HTML, or metadata for known URLs using the You.com Contents API.\n- Use this step when you already know the URLs. Use Web Search with livecrawl when You.com should discover pages from a query.\n- A single request supports up to 10 URLs.\n- Request only the formats you need. Markdown is recommended for LLM consumption.\n- Increase crawlTimeout for JavaScript-heavy pages, up to 60 seconds.\n- Individual pages can partially fail; check each returned item before processing.\n- Input: `{ urls: string[], formats?: ("markdown" | "html" | "metadata")[], crawlTimeout?: number }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComLiveNews\nFetch live news articles through the You.com Search API and return the full structured response.\n- Defaults freshness to day for breaking or recent news.\n- Use country and language together to monitor regional or non-English news.\n- Use livecrawl: \'news\' with livecrawlFormats: [\'markdown\'] when you need full article text.\n- Use a custom freshness range like YYYY-MM-DDtoYYYY-MM-DD for historical news windows.\n- Input: `{ query: string, freshness?: string, count?: number, country?: string, language?: string, safesearch?: "off" | "moderate" | "strict", livecrawl?: "news" | "all", livecrawlFormats?: ("markdown" | "html")[] }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComWebResearch\nAsk a research question and return a grounded You.com Research API answer with sources.\n- Use Web Search when you need raw URLs and snippets. Use Web Research when you want a synthesized answer with citations.\n- researchEffort controls depth and latency: lite, standard, deep, or exhaustive. standard is a good default.\n- sourceControl can restrict, exclude, or boost domains, and can apply freshness or country filters.\n- includeDomains cannot be combined with excludeDomains or boostDomains.\n- outputSchema returns structured output.content and is supported by standard, deep, and exhaustive, not lite.\n- Input: `{ input: string, researchEffort?: "lite" | "standard" | "deep" | "exhaustive", sourceControl?: { includeDomains?: string[], excludeDomains?: string[], boostDomains?: string[], freshness?: string, country?: string } | string, outputSchema?: object | string }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComWebSearch\nSearch the web and news using the You.com Search API and return the full structured response.\n- Query supports You.com search operators:\n| Operator | Description | Example |\n| -------- | ----------- | ------- |\n| site: | Search within a domain and its subdomains | site:uscourts.gov |\n| filetype: | Search for a specific file type | filetype:pdf |\n| + | Require the exact term after the operator | +GAAP |\n| - | Exclude the exact term after the operator | -prs |\n| AND | Require both expressions | guitar AND Fender |\n| OR | Match either expression | guitar OR drum |\n| NOT | Negate an expression | NOT site:uscourts.gov |\n- Use livecrawl with livecrawlFormats: [\'markdown\'] when you need full page content instead of snippets.\n- Use the Get Page Content step when you already know the URLs to fetch.\n- Use freshness for recency: day, week, month, year, or YYYY-MM-DDtoYYYY-MM-DD.\n- Use country (ISO 3166-1 alpha-2) and language (BCP 47) to focus results geographically or linguistically.\n- includeDomains cannot be combined with excludeDomains or boostDomains.\n- Input: `{ query: string, count?: number, freshness?: string, country?: string, language?: string, offset?: number, safesearch?: "off" | "moderate" | "strict", livecrawl?: "web" | "news" | "all", livecrawlFormats?: ("markdown" | "html")[], crawlTimeout?: number, includeDomains?: string[], excludeDomains?: string[], boostDomains?: string[] }`\n- Output: `{ data: object | unknown[] }`\n\n### YouTube\n\n#### fetchYoutubeCaptions\nRetrieve the captions/transcript for a YouTube video.\n- Supports multiple languages via the language parameter.\n- "text" export produces timestamped plain text; "json" export produces structured transcript data.\n- Input: `{ videoUrl: string, exportType: "text" | "json", language: string }`\n- Output: `{ transcripts: { text: string, start: number }[] }`\n\n#### fetchYoutubeChannel\nRetrieve metadata and recent videos for a YouTube channel.\n- Accepts a YouTube channel URL (e.g. https://www.youtube.com/@ChannelName or /channel/ID).\n- Returns channel info and video listings as a JSON object.\n- Input: `{ channelUrl: string }`\n- Output: `object`\n\n#### fetchYoutubeComments\nRetrieve comments for a YouTube video.\n- Paginates through comments (up to 5 pages).\n- "text" export produces markdown-formatted text; "json" export produces structured comment data.\n- Input: `{ videoUrl: string, exportType: "text" | "json", limitPages: string }`\n- Output: `{ comments: { id: string, link: string, publishedDate: string, text: string, likes: number, replies: number, author: string, authorLink: string, authorImg: string }[] }`\n\n#### fetchYoutubeVideo\nRetrieve metadata for a YouTube video (title, description, stats, channel info).\n- Returns video metadata, channel info, and engagement stats.\n- Video format data is excluded from the response.\n- Input: `{ videoUrl: string }`\n- Output: `object`\n\n#### searchYoutube\nSearch for YouTube videos by keyword.\n- Supports pagination (up to 5 pages) and country/language filters.\n- Use the filter/filterType fields for YouTube search parameter (sp) filters.\n- Input: `{ query: string, limitPages: string, filter: string, filterType: string, countryCode?: string, languageCode?: string }`\n- Output: `{ results: object }`\n\n#### searchYoutubeTrends\nRetrieve trending videos on YouTube by category and region.\n- Categories: "now" (trending now), "music", "gaming", "films".\n- Supports country and language filtering.\n- Input: `{ bp: "now" | "music" | "gaming" | "films", hl: string, gl: string }`\n- Output: `object`\n\n### Helpers\n\n#### `listModels()`\nList all available AI models across all categories.\n\nOutput:\n```typescript\n{\n models: {\n id: string;\n name: string; // Display name\n type: "llm_chat" | "image_generation" | "video_generation" | "video_analysis" | "text_to_speech" | "vision" | "transcription";\n maxTemperature: number;\n maxResponseSize: number;\n inputs: object[]; // Accepted input types\n }[]\n}\n```\n\n#### `listModelsByType(modelType)`\nList AI models filtered by type.\n- `modelType`: `"llm_chat"` | `"image_generation"` | `"video_generation"` | `"video_analysis"` | `"text_to_speech"` | `"vision"` | `"transcription"`\n- Output: same as `listModels()`\n\n#### `listModelsSummary()`\nList all available AI models (summary). Returns only id, name, type, and tags. Suitable for display or consumption inside a model context window.\n\nOutput:\n```typescript\n{\n models: {\n id: string;\n name: string;\n type: "llm_chat" | "image_generation" | "video_generation" | "video_analysis" | "text_to_speech" | "vision" | "transcription";\n tags: string; // Comma-separated tags\n }[]\n}\n```\n\n#### `listModelsSummaryByType(modelType)`\nList AI models (summary) filtered by type.\n- `modelType`: `"llm_chat"` | `"image_generation"` | `"video_generation"` | `"video_analysis"` | `"text_to_speech"` | `"vision"` | `"transcription"`\n- Output: same as `listModelsSummary()`\n\n#### `listConnectors()`\nList available OAuth connector services (Slack, Google, HubSpot, etc.) and their actions. These are third-party integrations \u2014 for most tasks, use actions directly instead.\n\nOutput:\n```typescript\n{\n services: {\n id: string;\n name: string;\n icon: string;\n actions: { id: string; name: string }[];\n }[]\n}\n```\n\n#### `getConnector(serviceId)`\nGet details for a single OAuth connector service by ID.\n\nOutput:\n```typescript\n{\n service: {\n id: string;\n name: string;\n icon: string;\n actions: { id: string; name: string }[];\n }\n}\n```\n\n#### `getConnectorAction(serviceId, actionId)`\nGet the full configuration for an OAuth connector action, including all input fields needed to call it via `runFromConnectorRegistry`. OAuth connectors are sourced from the open-source MindStudio Connector Registry (MSCR) with 850+ actions across third-party services.\n\nOutput:\n```typescript\n{\n action: {\n id: string;\n name: string;\n description: string;\n quickHelp: string;\n configuration: { title: string; items: { label: string; helpText: string; variable: string; type: string; defaultValue: string; placeholder: string; selectOptions?: object }[] }[];\n }\n}\n```\n\n#### `listConnections()`\nList OAuth connections for the organization (authenticated third-party service links). Use the returned connection IDs when calling OAuth connector actions. Connectors require the user to connect to the third-party service in MindStudio before they can be used.\n\nOutput:\n```typescript\n{\n connections: {\n id: string; // Connection ID to pass to connector actions\n provider: string; // Integration provider (e.g. slack, google)\n name: string; // Display name or account identifier\n }[]\n}\n```\n\n#### `estimateStepCost(stepType, step?, options?)`\nEstimate the cost of executing a step before running it. Pass the same step config you would use for execution.\n\n```typescript\nconst estimate = await mindstudio.estimateStepCost(\'generateText\', { message: \'Hello\' });\n```\n\n- `stepType`: string \u2014 The action name (e.g. `"generateText"`).\n- `step`: object \u2014 Optional action input parameters for more accurate estimates.\n- `options`: `{ appId?: string, workflowId?: string }` \u2014 Optional context for pricing.\n\nOutput:\n```typescript\n{\n costType?: string; // "free" when the step has no cost\n estimates?: {\n eventType: string; // Billing event type\n label: string; // Human-readable cost label\n unitPrice: number; // Price per unit in nanodollars (1e-9 USD)\n unitType: string; // What constitutes a unit (e.g. "token", "request")\n estimatedCost?: number; // Estimated total cost in nanodollars, or null if not estimable\n quantity: number; // Number of billable units\n }[]\n}\n```\n\n#### `changeName(displayName)`\nUpdate the display name of the authenticated agent. Useful for agents to set their own name after connecting.\n\n```typescript\nawait mindstudio.changeName(\'My Agent\');\n```\n\n#### `changeProfilePicture(profilePictureUrl)`\nUpdate the profile picture of the authenticated agent. Useful for agents to set their own avatar after connecting.\n\n```typescript\nawait mindstudio.changeProfilePicture(\'https://example.com/avatar.png\');\n```\n\n#### `uploadFile(content, options)`\n**Deprecated for app file storage** \u2014 use the `files` store below (`files.defineStore(...).put(...)`): private by default, app-scoped, and served on the app\'s own domain. `uploadFile` uploads to the shared account media CDN and remains only for account-level assets (e.g. an agent avatar for `changeProfilePicture`). Gets a signed upload URL, PUTs the file content, and returns the permanent public URL.\n\n```typescript\nimport { readFileSync } from \'fs\';\nconst { url } = await mindstudio.uploadFile(readFileSync(\'photo.png\'), { extension: \'png\', type: \'image/png\' });\n```\n\n- `content`: `Buffer | Uint8Array` \u2014 The file content.\n- `options.extension`: string \u2014 File extension without the dot (e.g. `"png"`, `"jpg"`, `"mp4"`).\n- `options.type`: string (optional) \u2014 MIME type (e.g. `"image/png"`). Determines which CDN subdomain is used.\n\nOutput: `{ url: string }` \u2014 The permanent public CDN URL.\n\n#### `files` \u2014 per-app file storage (private by default)\nTyped per-app file storage: user uploads, generated documents, images, marketing assets. Think of a store as a CDN-backed bucket the app talks to \u2014 NOT app-defined state like a `db` table. The API is *shaped* like the `db` namespace (define a store at module scope, then read/write), but its contents aren\'t modeled by code, and one store is shared across dev and prod (a file uploaded in the dev editor is already live in prod at the same URL). Creates are safe by default because keys default to unique (UUID, or a content-addressed hash); the operations to treat with care are `delete(key)` and overwriting a fixed key. Public objects are CDN-cached, and the CDN drops one when it is overwritten or deleted; its `Cache-Control`, set at put time via `cacheControl`, is how long a browser may keep a copy, at most an hour: auto-minted keys get the hour (the key is never reused), and named keys default to `public, max-age=300`, so an overwrite reaches every browser within ~5 minutes; pass `cacheControl` to tune (e.g. `\'public, max-age=60\'` for near-live config, `\'no-store\'` to revalidate every read). Files are served on the app\'s own domain: private stores (the default) are signed / session-authorized; public stores are world-readable, CDN-served, and images resize via query params (e.g. `?w=400&h=300&fit=cover`). Prefer this over the deprecated `uploadFile()` for app storage.\n\n```typescript\nimport { files } from \'@mindstudio-ai/agent\';\n\n// Define stores at module scope (access is pinned here).\nexport const Uploads = files.defineStore(\'uploads\'); // private (default)\nexport const Assets = files.defineStore(\'assets\', { access: \'public\' });\n\n// Store bytes and get a ready-to-use URL.\nconst file = await Uploads.put(buffer, { contentType: \'application/pdf\', filename: \'report.pdf\' });\nfile.url; // stable on-domain URL for a logged-in user (drop into <img>/<a download>)\nawait file.shareUrl(); // absolute signed URL that works with NO session (email/embed); default 24h\n```\n\nStore methods:\n- `put(content, { key?, contentType?, filename?, contentAddressed?, cacheControl? })` \u2192 `StoredFile`. `content` is `Buffer | Uint8Array | string`, or a stream (Node `Readable`, web `ReadableStream`, async iterable of bytes) \u2014 any size up to 5 GiB, straight to storage (a stream goes up a part at a time, and needs `key` or `filename`, not `contentAddressed`). Omit `key` for a random UUID; `contentAddressed: true` derives an immutable `<sha256>.<ext>` key. `cacheControl` sets how long a browser may keep a public object, at most an hour (defaults: auto-minted keys an hour, named keys `public, max-age=300`).\n- `putFile(path, options?)` \u2192 `StoredFile` \u2014 a file on disk, a part at a time (never held in memory): use it for anything large a method writes, like an ffmpeg output.\n- `importFromUrl(url, { contentType? })` \u2192 `StoredFile` \u2014 the platform fetches a direct link to a file straight into the store (up to 5 GiB, stored as it is, key chosen for it); never fetch a large file into a method to `put` it. A share page isn\'t the file; a hosted video is `downloadVideo`\'s.\n- `get(key)` \u2192 `Buffer` (the whole file in memory) \xB7 `download(key, path)` \u2192 `void` (streamed to disk) \xB7 `getStream(key, { start?, end? })` \u2192 web `ReadableStream` (all of it, or a byte range). All three read straight from storage; a missing key throws `file_not_found`.\n- `head(key)` \u2192 `StoredFile` \xB7 `exists(key)` \u2192 `boolean`\n- `list({ prefix?, cursor?, limit? })` \u2192 `{ files: StoredFile[]; cursor? }`\n- `delete(key)` \u2192 `void`\n- `shareUrl(key, { expiresIn? })` \u2192 signed absolute URL (private stores; no session needed)\n\n`StoredFile`: `{ store, key, access, size?, contentType?, updatedAt?, url, shareUrl() }`.\n\n`defineStore(name, options?)` options: `access: \'public\' | \'private\'` (default private), `maxSize` (bytes; cap for client-direct uploads), `contentTypes` (allowlist for client-direct uploads).\n\nUser uploads (client-direct \u2014 bytes go straight to storage, never through the app backend): a backend method mints a token, the frontend submits it with `@mindstudio-ai/interface`.\n```typescript\n// backend method\nconst token = await Uploads.createUploadToken({ contentType, maxSize: 25 * 1024 * 1024 }); // also accepts cacheControl\nreturn token;\n// frontend (@mindstudio-ai/interface)\nimport { platform } from \'@mindstudio-ai/interface\';\nconst { url } = await platform.upload(token, file, { onProgress });\n```\nA token must start its upload within 10 minutes of minting (`expiresIn`: 60 to 900 seconds). A file of 64 MB or more goes up in parts and resumes: `platform.upload` again with a token for the same `key` and the same file sends only what hadn\'t arrived (mint with a stable `key` to resume across a reload). In the frontend, `fetch(file.url)` works with default credentials, never `credentials: \'include\'`.\n\nGenerated assets: EVERY action that produces a file can put its output straight into one of these stores \u2014 pass the store handle as `store` in the OPTIONS object (second argument). There is no allowlist; if an action returns a file URL, it takes `store`. Optional: omit it and the asset goes to the shared MindStudio CDN as before. Reach for it when the asset belongs to the app \u2014 private to a user, or listed alongside its other files. Inputs are never published either way: a file you hand an action is read where it sits, and any intermediate the platform needs is written to private storage and dropped when the call finishes.\n\n```typescript\nconst { imageUrl } = await mindstudio.generateImage({ prompt: \'a red sneaker\' }, { store: Assets });\n```\n\n#### `db` \u2014 typed tables over the app\'s managed SQLite\n\nChainable, typed collection API over the app\'s managed databases. Define a table at module scope; every read/write is lazy until awaited, and reads/writes can be batched into one round trip with `db.batch()`.\n```typescript\nimport { db } from \'@mindstudio-ai/agent\';\n\nexport const Orders = db.defineTable<Order>(\'orders\');\n\n// Reads \u2014 filters compile to SQL WHERE when possible\nconst recent = await Orders.filter(o => o.status === \'paid\').sortBy(o => o.created_at).reverse().take(20);\n// Comparing to an outer-scope value? Use the bindings form so it compiles to SQL:\nconst mine = await Orders.filter((o, $) => o.requestedBy === $.uid, { uid: auth.userId }); // bindings: lifts closure var so filter compiles to SQL\n\n// Writes\nconst order = await Orders.push({ item: \'Laptop\', amount: 999, status: \'pending\' });\nawait Orders.update(order.id, { status: \'approved\' });\n```\n\nAggregation \u2014 `count()`, `sum(fn)`, `avg(fn)`, `countDistinct(fn)`, and `aggregate(spec)` compile to SQL aggregates (COUNT/TOTAL/AVG/GROUP BY); no rows are fetched, so they stay cheap at any table size. Never page a whole table into memory to compute a summary.\n```typescript\nconst top = await Answers\n .filter((a, $) => a.surveyId === $.surveyId, { surveyId }) // bindings: compiles to SQL\n .aggregate({\n by: [\'questionId\', \'dimension\'],\n select: { n: { count: true }, total: { sum: \'score\' }, avgScore: { avg: \'score\' }, respondents: { countDistinct: \'responseId\' } },\n orderBy: \'total\', desc: true, limit: 20,\n });\n// Array<{ questionId, dimension, n: number, total: number, avgScore: number | null, respondents: number }>\n```\nSelect terms: `{ count: true }` \xB7 `{ sum: \'col\' }` \xB7 `{ avg: \'col\' }` \xB7 `{ min: \'col\' }` \xB7 `{ max: \'col\' }` \xB7 `{ countDistinct: \'col\' }` (string column names). Omit `by` for whole-set aggregates (single object). Empty set: count/countDistinct 0, sum 0, avg/min/max null.\n\nRaw SQL escape hatch \u2014 `db.sql<T>(query, params?, { database? })` runs read-only raw SQL (joins, subqueries, window functions) against the app\'s own managed database. SELECT/WITH only; writes throw \u2014 use Table methods. Lazy and batchable. Last resort: prefer the typed API (including `aggregate()`) whenever it can express the query; raw rows may not exactly match the typed API\'s value representations.\n```typescript\nconst rows = await db.sql<{ questionId: string; n: number }>(\'SELECT questionId, COUNT(*) AS n FROM answers WHERE surveyId = ? GROUP BY questionId\', [surveyId]);\n```\n\n#### `dataSources` \u2014 searchable document corpora (RAG)\n\nDeclare a data source at module scope and import the handle. The platform owns parsing, chunking, embedding, storage and tenant isolation; every hit returns a citation.\n```typescript\nimport { dataSources } from \'@mindstudio-ai/agent\';\nexport const Policies = dataSources.defineDataSource(\'policies\');\n\n// search\nconst { results } = await Policies.search(\'what are the payment terms?\', { topK: 5 });\n// options: topK (default 5, max 50), scoreThreshold, filter, mode, maxPerDocument, highlight, rerank, hybrid\nconst context = results.map((r) => r.text).join(String.fromCharCode(10, 10));\n\n// narrow before ranking: metadata tags, filename, pages, required words\nawait Policies.search(\'termination clause\', {\n filter: { metadata: { department: \'legal\', signedAt: { gte: 20250101, lte: 20260630 } }, phrase: \'notice period\' },\n maxPerDocument: 2, // stop one document monopolizing the results\n});\n\n// mode: \'hybrid\' (default) | \'semantic\' | \'lexical\'. Lexical skips the query\n// embedding \u2014 fastest, right for identifier-shaped queries (error codes, SKUs).\nawait Policies.search(\'ERR-7741X\', { mode: \'lexical\' });\n\n// add a document (background \u2014 poll documents() for status)\nawait Policies.add(buffer, { filename: \'policy.pdf\', contentType: \'application/pdf\', metadata: { department: \'legal\' } });\nconst docs = await Policies.documents();\n\n// walk a large corpus (a page at a time behind the scenes) to build your own index of it\nfor await (const doc of Policies.allDocuments()) { /* doc.id, doc.metadata, doc.status */ }\n\n// introspection \u2014 what is in the corpus, and how one document was split\nconst { chunkCount, pipeline } = await Policies.stats(); // counts + the config actually in effect\nconst chunks = await Policies.chunks(docs[0].id); // why a document does or doesn\'t match\n\n// how many passages contain the words \u2014 exact, same filter grammar as search. Say\n// "N passages mention these words", never "N relevant results": no such total exists,\n// and search\'s hits are not this set (semantic hits need not contain the words).\nconst { chunks: mentions } = await Policies.count({ contains: \'client secret rotation\' });\n\n// what the matching passages are about: your own ids, asserted by add(), a mapper or a manifest\nconst { entities } = await Policies.entities({ filter: { entities: [\'vendor-acme\'] } }); // [{ id, chunks }], most first\n\n// the source\'s dictionary: your ids and their names, so passages are tagged wherever the text names them\nawait Policies.pushDictionary(vendors.map((v) => ({ id: v.id, names: [v.name, ...v.aliases] })));\nconst { mentions } = await Policies.link(\'Renewal terms agreed with ACME Corp.\'); // what it finds: [{ ids, text }]\n```\n\n`SearchHit`: `{ score, text, entities, citation: { documentId, filename, pageNumber, chunkIndex, headingPath, boundingBox?, url } }`. `citation.url` is a stable on-domain link to the source document for a signed-in user \u2014 drop it in an `<a href>`. A document is a cousin of a file and shares its share verbs: for a public app or another origin, `search(q, { shareCitations: 3600 })` returns every `citation.url` as an absolute signed link that needs no session, and `Policies.shareUrl(hit.citation, { expiresIn })` signs one document on demand (default 24h). `(documentId, chunkIndex)` is a chunk\'s stable identity \u2014 key an eval set on that, not on `text`.\n\nAlso on each hit: `retrievalRank`/`retrievalScore` (position before reranking \u2014 compare with the final position to see what reranking did). Three opt-in search options for debugging and display: `explain: true` adds `explain.{dense,lexical,matchedVia}` so you can see which half of hybrid found a hit; `expand: 1` adds `neighbors.{before,after}` for surrounding context; `highlight: true` adds `matches` \u2014 `{start, end}` offsets of query terms in `text`, for rendering highlights. None of them changes the results or their order.\n\nScore semantics: `score`\'s scale depends on the mode \u2014 cosine similarity (`semantic`), a rank-fusion reciprocal (`hybrid`; small numbers, not similarities), keyword-overlap weight (`lexical`) \u2014 and when reranking ran, it\'s the reranker\'s 0\u20131 relevance instead (the retrieval value stays in `retrievalScore`). So with reranking on (the default), put quality cutoffs on `score`. `scoreThreshold` cuts on the retrieval branch BEFORE fusion/reranking \u2014 a per-embedding-model cosine floor, not a relevance cutoff \u2014 so leave it unset unless you\'ve measured it on your corpus.\n\n`filter` fields (all AND together, and only narrow): `metadata` (per-key equals, any-of via an array, or a numeric range via `{gte?, lte?}`), `filename`, `documentIds`, `entities` (any of these ids), `pages: {min?, max?}`, `contains` (all words, any order), `phrase` (exact adjacent sequence). Document metadata is set at add time: scalars only, \u226416 keys, keys `[a-zA-Z0-9_-]`. Ranges are numeric only \u2014 to filter by date, store dates as sortable integers (`20250315`-style YYYYMMDD, or epoch seconds) and range on those. Re-adding the same bytes with different metadata or entities updates them in place \u2014 no re-processing.\n\nA document\'s `entities` name what it is about as the app\'s own ids (an asset tag, a person\'s row id), whether or not its text says so: set by `add(bytes, { entities })`, a mapper\'s `entities` or a job manifest line\'s, up to 256 per document, each 1\u2013128 printable characters with no spaces or commas, and carried by every passage. Metadata describes a document; entities say what it is about, and the app\'s own tables say how those things relate. `filter.entities` narrows to passages about any of the ids. `entities({ filter?, limit? })` lists which entities the matching passages are about, most first, as `{ id, chunks }`: approximate counts of passages, not documents (limit default 20, max 1,000; `filter: { documentIds: [id] }` for one document\'s). Filter on one entity to see what appears beside it.\n\nA source\'s DICTIONARY tags passages by what their text says: `pushDictionary(entries)` sends every `{ id, names }` the app has (a projection of its own tables: up to 250,000 entries, 16 names each, a million names in all) and replaces the dictionary; from then on every passage carries the ids whose names it mentions, beside its document\'s asserted ones, and `filter.entities`, hits and `entities()` see both. Matching is deterministic and model-free: case and a tag\'s separators don\'t matter (`P-101A`, `P101A`, `p 101a`), nor a size\'s inch or foot mark before one (`8"-P-1057-A1A` is `8-P-1057-A1A`), a name matches only as whole words (`P-101AB` is not `P-101A`), the longest name wins, and a name two ids claim tags both. A name needs three letters or digits (two in Chinese, Japanese or Korean). Passages indexed before a push are re-tagged in the background (a relink, nothing re-embedded; `dictionary()` shows it). Push it whole after each load of the tables it comes from: an unchanged dictionary commits nothing (`unchanged: true`). The result reports `ambiguousNames`, `mostClaimed` (each `{ key, name, ids }`: the folded key and one name as pushed) and names that can\'t match their own text (`unmatchable`). `link(text)` shows what the current dictionary finds in any text; use it to check a dictionary before relying on it. A mapper may not push one.\n\nSearch is deterministic for a fixed corpus and configuration \u2014 same query, same order \u2014 so regression checks are meaningful. `search()` also returns `latencyMs`.\n\n`search()` throws `index_warming` (HTTP 503) when a large corpus\'s index is being reloaded after an eviction \u2014 a minute or two, in the background. It means loading, not empty: tell the user the knowledge base is warming up and retry shortly. Small corpora reload inside the search and never raise it; sources on dedicated capacity are never evicted.\n\nOn dedicated capacity `search()` throws `index_building` (HTTP 503) instead, while a bulk load runs and while the index is built after one: the index is built once at the end of a load rather than behind every write, and the message carries "N of M vectors indexed". Handle it like `index_warming` \u2014 the knowledge base is being built, never empty; say so and retry later.\n\nOther methods: `stats()`, `documents()` (the first thousand, or `documents({ ids })` to poll what `add` returned), `documentsPage({ cursor?, limit?, order? })` (one page; `order: "newest"` is what just arrived) and `allDocuments()` (walk a corpus of any size, oldest first; to write what each document says into the app\'s own tables, give the source a READER instead), `passages({ filter?, cursor?, limit? })` and `allPassages(filter?)` (every passage matching a filter, unranked, document by document, as `{ text, entities, citation }`: every page naming an entity rather than search\'s best few; limit default 50, max 200), `chunks(documentId, {vectors?})`, `remove(documentId)`, `removeWhere(selector)`, `ensure(name?)` (rarely needed \u2014 `add` and `search` create-on-reference), and `DataSource.contentHash(bytes)` to check for an existing document before adding.\n\n`removeWhere({ metadata?, filename?, documentIds?, externalIdPrefix? })` removes every matching document (vectors, row and bytes) in pages of a thousand and returns `{ deleted, remaining }`. Chunk-level filter fields, `entities` and an empty selector are refused; deleting a whole source is an owner operation (`remy-admin datasources delete`), not something app code does.\n\nKeeping a corpus in sync with an S3 bucket: the owner connects the bucket once with `remy-admin datasources connect --source <slug> --bucket <b> --region <r> --prefix <p> --access-key-secret <NAME> --secret-key-secret <NAME>` (the two names are app secrets set with `remy-admin secrets set`; credentials are never written in code or pasted in chat). From then on `await Source.sync()` lists the bucket, diffs it by ETag against what was ingested, and runs a job over what changed \u2014 returning `{ job }` immediately; poll `Source.job(id)` or list `Source.jobs()`. A nightly sync is an ordinary cron interface job whose method calls `sync()`; nothing is spent when nothing changed. The job auto-approves under the connector\'s per-sync budget; a plan over it waits in `planned` for the owner to `remy-admin datasources jobs approve <id>`.\n\nWhen the file is not the document \u2014 a JSON record that should become markdown plus metadata, a JSONL object holding many articles, a kill notice that means "remove this story" \u2014 give the source a MAPPER: `datasources/<slug>.mapper.ts` exporting `defineMapper(Source, { map })` from `@mindstudio-ai/agent`, declared in mindstudio.json as `"dataSources": [{ "slug": "<slug>", "mapper": { "path": "dist/datasources/<slug>.mapper.ts" } }]`. `map(object)` gets `{ key, size, contentType, etag, lastModified, metadata, entities, bytes(), text(), json() }` and returns one of `documents([{ externalId, title, markdown, metadata?, entities?, replaces? }])`, `passthrough({ metadata?, entities? })` (ingest the raw object as-is), `skip(reason)`, or `deletes([externalId, ...])`; throwing is the object\'s error. `documents()` takes an array or any iterable or async iterable with no cap on its length, so a generator that yields a document per row streams an export of any size; a document that fails validation is left out and named, the rest go in. Everything entering a mapped source is mapped \u2014 jobs, syncs and `add()` alike \u2014 so `add()` on a mapped source returns `documents` (possibly several, with `invalid` naming any that failed) and throws `mapper_skipped` for an object the mapper refused; an object that maps to more than a thousand documents registers the first thousand and returns `job`, which takes the rest (`documentCount` is the total). A mapper may call models (`runTask`) or `fetch`; every call is spend per object. The owner tests one with `remy-admin datasources map test --source <slug> --dev` before deploying, reads what a job skipped with `datasources jobs quarantine <id>`, and re-applies a fixed mapper with `datasources remap --source <slug>`. See the dataSources skill for the authoring loop.\n\nTo write what each document says into the app\'s own tables (the equipment a passage names, the relations it states, the people it quotes), give the source a READER: `datasources/<slug>.reader.ts` exporting `defineReader(Source, { read, removed, cleared? })` from `@mindstudio-ai/agent`, declared in mindstudio.json as `"dataSources": [{ "slug": "<slug>", "reader": { "path": "dist/datasources/<slug>.reader.ts", "timeoutMs"?: 60000, "concurrency"?: 16 } }]` (beside or instead of a mapper). The platform runs it over every document the source builds, however it arrived (`add()`, a sync or a job), with its own batching, retries, budgets and pauses: `read(document)` gets `{ id, sourceId, filename, contentType, externalId, metadata, entities, pageCount, chunkCount, createdAt, url, chunks(), bytes() }`, where `chunks()` returns the passages as indexed, each with its entities, and `bytes()` (or the short-lived `url`) the file the document came from, for a reader that needs the drawing itself; `removed(documentId)` runs when a document leaves the source; `cleared(sourceId)` once when the source is deleted; the optional `relinked(document)` when a new dictionary re-tagged a document already read (the same document, with its new entities; for rows derived from the tags, not a reason to extract again). A document can be read more than once, so `read` replaces that document\'s rows as a set, keyed by its id, and `removed` deletes them. A throw fails that document and it is retried. The reader always runs against the LIVE release (its code and its database) and starts reading once the app publishes it; it may call models and `fetch`, and every call is spend per document. A reader may not add to, remove from or push a dictionary to its own source. The owner tests one with `remy-admin datasources reader test --source <slug> --dev`, sees its deliveries with `datasources reader status`, and reads what it hasn\'t with `datasources reread --source <slug> --unread`, a job priced before it runs. See the dataSources skill for the authoring loop.\n\nSupported: pdf, docx, pptx, xlsx, odt, rtf, epub, images, txt, md, json, csv, tsv, log, html.\n\nAdding the same bytes twice is free \u2014 documents are content-addressed, so a re-add is a no-op when this source has already processed those exact bytes under its current configuration. Only reconfiguring the source makes a re-add do work again, and that is an explicit owner-triggered migration, never a side effect of a deploy.\n\nEverything in `SearchOptions` \u2014 `mode`, `filter`, `rerank`, `hybrid`, `maxPerDocument`, `highlight` \u2014 is free to change and takes effect on the next search. How documents are chunked and embedded is configured on the corpus itself with `remy-admin datasources config` \u2014 not in code \u2014 and changing it on a populated source is rejected rather than silently rebuilding.\n\n**A data source is live and shared.** Unlike database tables there is no dev copy and no per-release isolation: adding or removing a document changes what the deployed app retrieves, immediately. Scenarios never reset a data source, and re-ingesting a large corpus costs real money.\n\n#### `analytics` \u2014 read the app\'s own traffic + event analytics\n\nThe platform auto-tracks pageviews (and custom events via the frontend\'s `analytics.track()`); the backend `analytics` namespace is the read side, so app code can correlate real traffic with its own data \u2014 lifetime views next to each blog post, "N people reading now", conversions beside the records that produced them. Backend only (hook token); reads the LIVE app\'s real traffic even from a dev sandbox. Rate limited per app (600 reads/min; the SDK auto-retries one 429). For pages composed of several reads, use `analytics.batch([spec, ...])` \u2014 up to 10 queries in one round trip, one rate token each, results in request order \u2014 instead of a parallel burst of `query()` calls competing with each other. NEVER swallow `rate_limited` errors into empty data: a page silently rendering zeros is worse than surfacing a retry state.\n\nOne general read, `query()`, shaped like the Plausible/GA4 query APIs:\n\n```typescript\nimport { analytics } from \'@mindstudio-ai/agent\';\n\n// Lifetime pageviews + unique visitors per page \u2014 top 10.\nconst top = await analytics.query({\n metrics: [\'pageviews\', \'visitors\'],\n dimensions: [\'path\'],\n dateRange: \'all\',\n limit: 10,\n});\n// top.results[0] \u2192 { dimensions: { path: \'/post/hello\' }, metrics: { pageviews: 4210, visitors: 1830 } }\n\n// Daily views for one post, full history.\nconst series = await analytics.query({\n metrics: [\'pageviews\'],\n dimensions: [\'time\'],\n granularity: \'day\',\n filters: [[\'is\', \'path\', [\'/post/hello\']]],\n dateRange: \'all\',\n});\n```\n\nGrammar: `metrics` from `pageviews | visitors | visits | events`; `dimensions` is at most ONE entity dimension (`path`, `referrerHost`, `sourceCategory`, `country`, `city`, `deviceType`, `browser`, `os`, `language`, `visitorType`, `utmSource`\u2026`utmContent`, `eventName`) OR `\'time\'` (with `granularity: \'5m\'|\'hour\'|\'day\'|\'week\'|\'month\'` and optional IANA `timezone`); `filters` are `[op, dimension, values]` triples with ops `is | is_not | contains`; `dateRange` is a shorthand (`\'1h\'|\'24h\'|\'7d\'|\'30d\'|\'90d\'|\'all\'`) or `[startISO, endISO]`; `orderBy` (grouped only) accepts `pageviews` or `events`. With `dateRange: \'all\'`, granularity is a minimum \u2014 the server coarsens as history grows. `city` filter values are plain city names (any country) and always read the 90-day event detail.\n\n**Retention split \u2014 the one thing to get right:** queries whose filters are all `is` and touch at most one dimension read a rollup kept forever (full lifetime history, exact unique visitors). Cross-dimension intersections, `is_not`, and `contains` scan raw events retained for 90 days; the server clamps the window and reports it. Check `meta`: `source` (`rollup` vs `events`), `clamped`, `window.served`, and `metricsOmitted` \u2014 metrics a shape can\'t carry (e.g. `visits` on grouped reads) are omitted there, never returned as silent zeros. `meta.total` carries the unpaginated group count for paging.\n\nSpecials (same `dateRange`/filter-triple grammar): `analytics.live()` \u2192 `{count, countries, sparkline}` right now; `analytics.sources(opts?)` \u2192 per-session first-source ranking (UTM > referrer > direct) classified by category/vendor incl. AI assistants; `analytics.map(opts?)` \u2192 city lat/lon points; `analytics.aiSources(opts?)` \u2192 AI-assistant referrals per vendor; `analytics.crawlers.overview()/timeseries()/recent()` \u2192 bot/AI-crawler ingestion of the app\'s pages.\n\n#### `events` \u2014 realtime over named channels (publish/subscribe)\n\nPublish to named channels from any backend code (a method, a cron, a webhook handler); connected clients receive the payloads instantly over a platform-held stream \u2014 live dashboards, notifications, chat, collaborative UIs, multi-tab/device sync, with no polling and no WebSocket code. Different from `stream()`, which narrates ONE invocation to the caller currently waiting on it: events reach clients that were not part of the invocation at all. Backend only (hook token).\n\n```typescript\nimport { auth, events } from \'@mindstudio-ai/agent\';\n\n// The subscribe door is one of YOUR methods \u2014 auth checks first, then mint.\n// The grant is the ENTIRE subscribe-side authorization: whoever holds the\n// token receives those channels\' events for its lifetime.\nexport async function watchInbox() {\n auth.requireRole(\'member\');\n return await events.grant(`user:${auth.userId}`); // { token, expiresAt, ttlSeconds }; ttlSeconds? 60\u20133600, default 900\n}\n\n// Anything can publish \u2014 one call, up to 500 channels:\nconst members = await RoomMembers.where({ roomId });\nconst { delivered } = await events.publish(\n members.map((m) => `user:${m.userId}`),\n { type: \'message\', roomId, msgId },\n);\n```\n\nThe frontend consumes with `events.connect({ getToken, onEvent, onConnect })` from `@mindstudio-ai/interface` \u2014 `getToken` calls the minting method, and the SDK handles reconnection and grant renewal.\n\n**Ephemeral client signals (cursors, typing, live strokes):** mint the grant with `publish` channels \u2014 `events.grant(channel, { publish: [channel] })` \u2014 and the frontend publishes DIRECTLY via `sub.publish(channels, data)`: no backend method runs per signal, the SDK coalesces to ~40ms batches, and failures drop (never retry). Client events cap at 8k serialized, 30 batches/sec per grant. Carry a client `seq` in the payload (delivery is unordered across batches; receivers drop stale seqs and interpolate). Durable state still commits through a method with an id-only nudge \u2014 signal ephemerally, commit through code.\n\n**A channel is an audience, and a method decides who is in it.** Never put two users\' data on one channel. Default to per-user channels with publish-time fan-out (as above) for membership-gated audiences \u2014 removing someone stops their events immediately; reserve a shared channel for genuinely broadcast content (a ticker, a live blog), where revocation waits out the grant TTL. For anonymous visitors `auth.userId` is null \u2014 key their channels on `session.visitorId` instead, or `user:null` becomes one channel shared by every anonymous user.\n\n**Events are at-most-once nudges.** Nothing is buffered while a client is disconnected and nothing replays on connect \u2014 clients refetch state in `onConnect` (subscribe for speed, reconcile for truth). `delivered: 0` means nobody is listening right now: normal, never an error. Every delivered frame carries the platform-stamped publish `id` (consumer dedupe key: `id` + `channel`); `publish` returns the same id. Payloads cap at 256k serialized chars, checked in the SDK before the network so oversize throws locally \u2014 when a publish carries data, publish before committing the write it announces; high-rate paths publish ids (`{ type, id }`) and let the client fetch. Channels are exact strings (letters, digits, `: _ - .` \u2014 no wildcards), \u2264500 per publish, \u2264100 per grant. Publishes and grants are scoped to the execution\'s environment (live/preview/dev) automatically, so a dev-session publish can never reach live users. Debug with `remy-admin events tail | publish | channels list`.\n';
9277
- }
9278
- });
9279
-
9280
- // src/ask/prompt/index.ts
9281
- async function buildSystemPrompt(agent) {
9282
- const [modelsResult, connectionsResult, connectorsResult, llmsResult] = await Promise.allSettled([
9283
- agent.listModelsSummary(),
9284
- agent.listConnections(),
9285
- agent.listConnectors(),
9286
- Promise.resolve().then(() => (init_llms_content(), llms_content_exports))
9287
- ]);
9288
- const modelsSummary = modelsResult.status === "fulfilled" ? modelsResult.value.models.map(
9289
- (m) => `- ${m.id} (${m.name}, type: ${m.type}${m.popularity != null ? ", popularity: " + m.popularity : ""}${m.tags ? ", tags: " + m.tags : ""}${m.retired ? ", retired, replaced by " + m.replacedBy : m.retiresOn ? ", retires " + m.retiresOn + ", replaced by " + m.replacedBy : ""})`
9290
- ).join("\n") : "(Could not load models \u2014 use the listModels tool to look them up)";
9291
- const taskModels = modelsResult.status === "fulfilled" ? modelsResult.value.models.filter((m) => m.supportsToolUse && m.type === "llm_chat").map((m) => `- ${m.id} (${m.name})`).join("\n") || "(The platform reported no tool-use models \u2014 use the listModels tool.)" : "(Could not load models \u2014 use the listModels tool and check supportsToolUse)";
9292
- const connections = connectionsResult.status === "fulfilled" && connectionsResult.value.connections.length > 0 ? connectionsResult.value.connections.map((c) => `- ${c.provider}: ${c.name} (id: ${c.id})`).join("\n") : "No OAuth connections configured.";
9293
- const connectorServices = connectorsResult.status === "fulfilled" ? connectorsResult.value.services.map(
9294
- (s) => `- ${s.id}: ${s.name} (${s.actions?.length ?? 0} actions)`
9295
- ).join("\n") : "(Could not load connectors \u2014 use the getConnectorDetails tool)";
9296
- const llmsContent2 = llmsResult.status === "fulfilled" ? llmsResult.value.llmsContent : "(Could not load action reference \u2014 use getActionDetails tool)";
9297
- const referenceDocs = buildReferenceDocs({
9298
- modelsSummary,
9299
- taskModels,
9300
- connections,
9301
- connectorServices,
9302
- llmsContent: llmsContent2
9303
- });
9304
- return `${identity}
9305
-
9306
- ${referenceDocs}
9307
-
9308
- ${playbook}
9309
-
9310
- ${instructions}
9311
-
9312
- <!-- cache_breakpoint -->`;
9313
- }
9314
- var init_prompt = __esm({
9315
- "src/ask/prompt/index.ts"() {
9316
- "use strict";
9317
- init_identity();
9318
- init_reference();
9319
- init_instructions();
9320
- init_playbook();
9321
9203
  }
9322
9204
  });
9323
9205
 
@@ -9327,27 +9209,18 @@ __export(ask_exports, {
9327
9209
  cmdAsk: () => cmdAsk,
9328
9210
  runAsk: () => runAsk
9329
9211
  });
9330
- function resolveCredentials(options) {
9331
- const config = loadConfig();
9332
- const apiKey = process.env.CALLBACK_TOKEN ?? options.apiKey ?? process.env.MINDSTUDIO_API_KEY ?? config.apiKey;
9333
- if (!apiKey) {
9334
- throw new Error(
9335
- "Not authenticated. Run `mindstudio login` or set MINDSTUDIO_API_KEY."
9336
- );
9337
- }
9338
- const baseUrl = options.baseUrl ?? process.env.MINDSTUDIO_BASE_URL ?? process.env.REMOTE_HOSTNAME ?? config.baseUrl ?? DEFAULT_BASE_URL2;
9339
- return { apiKey, baseUrl };
9340
- }
9341
9212
  async function runAsk(question, options = {}, onEvent) {
9342
- const { apiKey, baseUrl } = resolveCredentials(options);
9343
9213
  const agent = new MindStudioAgent({
9344
9214
  apiKey: options.apiKey,
9345
9215
  baseUrl: options.baseUrl
9346
9216
  });
9347
- const system = await buildSystemPrompt(agent);
9217
+ const { baseUrl, token: apiKey } = agent._httpConfig;
9218
+ const system = await fetchAskContext({ baseUrl, apiKey });
9348
9219
  const messages = [{ role: "user", content: question }];
9349
9220
  let emptyRetries = 0;
9350
- while (true) {
9221
+ let billingCost = 0;
9222
+ const billingEvents = [];
9223
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
9351
9224
  let assistantText = "";
9352
9225
  const toolCalls = [];
9353
9226
  let stopReason = null;
@@ -9377,6 +9250,8 @@ async function runAsk(question, options = {}, onEvent) {
9377
9250
  break;
9378
9251
  case "done":
9379
9252
  stopReason = event.stopReason;
9253
+ billingCost += event.cost ?? 0;
9254
+ billingEvents.push(...event.billingEvents ?? []);
9380
9255
  break;
9381
9256
  case "error":
9382
9257
  throw new Error(event.error);
@@ -9402,7 +9277,7 @@ async function runAsk(question, options = {}, onEvent) {
9402
9277
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0
9403
9278
  });
9404
9279
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
9405
- return assistantText;
9280
+ return { answer: assistantText, billingCost, billingEvents };
9406
9281
  }
9407
9282
  const results = await Promise.all(
9408
9283
  toolCalls.map(async (tc) => {
@@ -9420,6 +9295,9 @@ async function runAsk(question, options = {}, onEvent) {
9420
9295
  });
9421
9296
  }
9422
9297
  }
9298
+ throw new Error(
9299
+ `The answer took more than ${MAX_TURNS} turns. Ask again, or split the question.`
9300
+ );
9423
9301
  }
9424
9302
  function summarizeInput(input) {
9425
9303
  const vals = Object.values(input).filter((v) => typeof v === "string");
@@ -9429,7 +9307,7 @@ function summarizeInput(input) {
9429
9307
  async function cmdAsk(question, options) {
9430
9308
  try {
9431
9309
  let lineBuffer = "";
9432
- const response = await runAsk(question, options, (event) => {
9310
+ const result = await runAsk(question, options, (event) => {
9433
9311
  switch (event.type) {
9434
9312
  case "text":
9435
9313
  lineBuffer += event.text;
@@ -9458,10 +9336,18 @@ async function cmdAsk(question, options) {
9458
9336
  if (lineBuffer) {
9459
9337
  process.stderr.write(lineBuffer);
9460
9338
  }
9461
- if (process.stdout.isTTY) {
9339
+ if (options.json) {
9340
+ process.stdout.write(
9341
+ JSON.stringify({
9342
+ answer: result.answer,
9343
+ $billingCost: result.billingCost,
9344
+ $billingEvents: result.billingEvents
9345
+ }) + "\n"
9346
+ );
9347
+ } else if (process.stdout.isTTY) {
9462
9348
  process.stderr.write("\n");
9463
9349
  } else {
9464
- process.stdout.write(response + "\n");
9350
+ process.stdout.write(result.answer + "\n");
9465
9351
  }
9466
9352
  } catch (err) {
9467
9353
  process.stderr.write(`Error: ${err.message}
@@ -9469,17 +9355,16 @@ async function cmdAsk(question, options) {
9469
9355
  process.exit(1);
9470
9356
  }
9471
9357
  }
9472
- var DEFAULT_BASE_URL2, EMPTY_TURN_RETRIES, color, ansi;
9358
+ var EMPTY_TURN_RETRIES, MAX_TURNS, color, ansi;
9473
9359
  var init_ask = __esm({
9474
9360
  async "src/ask/index.ts"() {
9475
9361
  "use strict";
9476
- init_config();
9477
9362
  await init_client();
9363
+ init_context2();
9478
9364
  init_sse();
9479
9365
  init_tools();
9480
- init_prompt();
9481
- DEFAULT_BASE_URL2 = "https://v1.mindstudio-api.com";
9482
9366
  EMPTY_TURN_RETRIES = 1;
9367
+ MAX_TURNS = 24;
9483
9368
  color = process.stderr.isTTY;
9484
9369
  ansi = {
9485
9370
  dim: (s) => color ? `\x1B[2m${s}\x1B[0m` : s,
@@ -9549,7 +9434,7 @@ async function startMcpServer(options) {
9549
9434
  capabilities: { tools: {} },
9550
9435
  serverInfo: {
9551
9436
  name: "mindstudio-agent",
9552
- version: "0.1.162"
9437
+ version: "0.1.164"
9553
9438
  },
9554
9439
  instructions: 'Welcome to MindStudio \u2014 a platform with 200+ AI models, 850+ third-party integrations, and pre-built agents.\n\nGetting started:\n1. Call `ask` with any question about the SDK \u2014 it knows every action, model, and connector and returns working code with real model IDs and config options. Examples: ask("generate an image with FLUX"), ask("what models support vision?"), ask("how do I send a Slack message?").\n2. Call `changeName` to set your display name \u2014 use your name or whatever your user calls you. This is how you\'ll appear in MindStudio request logs.\n3. If you have a profile picture, pass its public image URL to `changeProfilePicture`.\n4. For manual browsing, call `listActions` to discover all available actions.\n\nThen use the tools to generate text, images, video, audio, search the web, work with data sources, run agents, and more.\n\nImportant:\n- AI-powered actions (text generation, image generation, video, audio, etc.) cost money. Before running these, call `estimateActionCost` and confirm with the user before proceeding \u2014 unless they\'ve explicitly told you to go ahead.\n- Not all agents from `listAgents` are configured for API use. Do not try to run an agent just because it appears in the list \u2014 it will likely fail. Only run agents the user specifically asks you to run.'
9555
9440
  });
@@ -9566,7 +9451,7 @@ async function startMcpServer(options) {
9566
9451
  let result;
9567
9452
  if (toolName === "ask") {
9568
9453
  const { runAsk: runAsk2 } = await init_ask().then(() => ask_exports);
9569
- result = await runAsk2(args.question, options);
9454
+ result = (await runAsk2(args.question, options)).answer;
9570
9455
  } else if (toolName === "listActions") {
9571
9456
  const meta = await getMetadata();
9572
9457
  const summary = {};
@@ -9656,7 +9541,13 @@ async function startMcpServer(options) {
9656
9541
  result = await getAgent().executeStep(step.stepType, args);
9657
9542
  }
9658
9543
  sendResult(id, {
9659
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
9544
+ content: [
9545
+ {
9546
+ type: "text",
9547
+ // Text as it is (ask's markdown answer); anything else as JSON.
9548
+ text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
9549
+ }
9550
+ ]
9660
9551
  });
9661
9552
  } catch (err) {
9662
9553
  const message = err instanceof MindStudioError ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
@@ -10470,7 +10361,7 @@ function isNewerVersion(current, latest) {
10470
10361
  return false;
10471
10362
  }
10472
10363
  async function checkForUpdate() {
10473
- const currentVersion = "0.1.162";
10364
+ const currentVersion = "0.1.164";
10474
10365
  if (!currentVersion || process.env.MINDSTUDIO_NO_UPDATE_CHECK) {
10475
10366
  return null;
10476
10367
  }
@@ -10505,7 +10396,7 @@ async function checkForUpdate() {
10505
10396
  }
10506
10397
  }
10507
10398
  function printUpdateNotice(latestVersion) {
10508
- const currentVersion = "0.1.162";
10399
+ const currentVersion = "0.1.164";
10509
10400
  process.stderr.write(
10510
10401
  `
10511
10402
  ${ansi2.cyanBright("Update available")} ${ansi2.gray(currentVersion + " \u2192")} ${ansi2.cyanBold(latestVersion)}
@@ -10518,7 +10409,7 @@ function isStandaloneBinary() {
10518
10409
  return !argv1.includes("node_modules");
10519
10410
  }
10520
10411
  async function cmdUpdate() {
10521
- const currentVersion = "0.1.162";
10412
+ const currentVersion = "0.1.164";
10522
10413
  process.stderr.write(
10523
10414
  ` ${ansi2.gray("Current version:")} ${currentVersion}
10524
10415
  `
@@ -10634,12 +10525,12 @@ function maskKey(key) {
10634
10525
  return key.slice(0, 4) + "..." + key.slice(-4);
10635
10526
  }
10636
10527
  async function cmdLogin(options) {
10637
- const baseUrl = options.baseUrl ?? process.env.MINDSTUDIO_BASE_URL ?? process.env.REMOTE_HOSTNAME ?? DEFAULT_BASE_URL3;
10528
+ const baseUrl = options.baseUrl ?? process.env.MINDSTUDIO_BASE_URL ?? process.env.REMOTE_HOSTNAME ?? DEFAULT_BASE_URL2;
10638
10529
  process.stderr.write("\x1B[2J\x1B[H");
10639
10530
  process.stderr.write("\n");
10640
10531
  printLogo();
10641
10532
  process.stderr.write("\n");
10642
- const ver = "0.1.162";
10533
+ const ver = "0.1.164";
10643
10534
  process.stderr.write(
10644
10535
  ` ${ansi2.bold("MindStudio Agent")} ${ver ? " " + ansi2.gray("v" + ver) : ""}
10645
10536
  `
@@ -10714,7 +10605,7 @@ async function cmdLogin(options) {
10714
10605
  const config = {
10715
10606
  apiKey: result.apiKey
10716
10607
  };
10717
- if (baseUrl !== DEFAULT_BASE_URL3) {
10608
+ if (baseUrl !== DEFAULT_BASE_URL2) {
10718
10609
  config.baseUrl = baseUrl;
10719
10610
  }
10720
10611
  saveConfig2(config);
@@ -10951,7 +10842,7 @@ async function main() {
10951
10842
  const updatePromise = command !== "mcp" && command !== "login" && command !== "update" ? checkForUpdate() : Promise.resolve(null);
10952
10843
  try {
10953
10844
  if (command === "version" || command === "-v") {
10954
- process.stdout.write("0.1.162\n");
10845
+ process.stdout.write("0.1.164\n");
10955
10846
  return;
10956
10847
  }
10957
10848
  if (command === "login") {
@@ -10990,6 +10881,7 @@ async function main() {
10990
10881
  "Usage:",
10991
10882
  ' mindstudio ask "your question here"',
10992
10883
  ' echo "your question" | mindstudio ask',
10884
+ ' mindstudio ask --json "..." # { answer, $billingCost, $billingEvents } on stdout',
10993
10885
  "",
10994
10886
  "Examples:",
10995
10887
  ' mindstudio ask "generate an image with FLUX"',
@@ -11004,7 +10896,8 @@ async function main() {
11004
10896
  const { cmdAsk: cmdAsk2 } = await init_ask().then(() => ask_exports);
11005
10897
  await cmdAsk2(question, {
11006
10898
  apiKey: values["api-key"],
11007
- baseUrl: values["base-url"]
10899
+ baseUrl: values["base-url"],
10900
+ json: values.json
11008
10901
  });
11009
10902
  return;
11010
10903
  }
@@ -11405,7 +11298,7 @@ async function main() {
11405
11298
  }
11406
11299
  }
11407
11300
  }
11408
- var HELP, MIME_TYPES2, ansi2, UPDATE_CHECK_INTERVAL, LOGO, DEFAULT_BASE_URL3, SPINNER_FRAMES, GLOBAL_STRING_FLAGS, GLOBAL_BOOLEAN_FLAGS;
11301
+ var HELP, MIME_TYPES2, ansi2, UPDATE_CHECK_INTERVAL, LOGO, DEFAULT_BASE_URL2, SPINNER_FRAMES, GLOBAL_STRING_FLAGS, GLOBAL_BOOLEAN_FLAGS;
11409
11302
  var init_cli = __esm({
11410
11303
  "src/cli.ts"() {
11411
11304
  "use strict";
@@ -11516,7 +11409,7 @@ Examples:
11516
11409
  =@@@@@@@-.@@@@@@@#.-@@@@@@+
11517
11410
  :@@@@@@: +@@@@@#. .@@@@@@:
11518
11411
  .++: .-*-. .++:`;
11519
- DEFAULT_BASE_URL3 = "https://v1.mindstudio-api.com";
11412
+ DEFAULT_BASE_URL2 = "https://v1.mindstudio-api.com";
11520
11413
  SPINNER_FRAMES = [
11521
11414
  "\u28FE",
11522
11415
  "\u28FD",