@mindstudio-ai/agent 0.1.127 → 0.1.129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +763 -325
- package/dist/index.d.ts +13 -3
- package/dist/index.js +616 -221
- package/dist/index.js.map +1 -1
- package/dist/postinstall.js +763 -325
- package/llms.txt +4 -4
- package/package.json +7 -8
package/dist/cli.js
CHANGED
|
@@ -198,8 +198,8 @@ var init_metadata = __esm({
|
|
|
198
198
|
},
|
|
199
199
|
"deleteGoogleCalendarEvent": {
|
|
200
200
|
stepType: "deleteGoogleCalendarEvent",
|
|
201
|
-
description: "
|
|
202
|
-
usageNotes: "- Requires a Google OAuth connection with Calendar events scope.\n-
|
|
201
|
+
description: "Delete an event from a Google Calendar by event ID.",
|
|
202
|
+
usageNotes: "- Requires a Google OAuth connection with Calendar events scope.\n- Throws an error if the event does not exist.",
|
|
203
203
|
inputSchema: { "type": "object", "properties": { "connectionId": { "type": "string", "description": "Google OAuth connection ID" }, "eventId": { "type": "string", "description": "Google Calendar event ID to delete" }, "calendarId": { "type": "string", "description": 'Calendar ID (defaults to "primary" if omitted)' } }, "required": ["eventId"] },
|
|
204
204
|
outputSchema: { "description": "This step does not produce output data." }
|
|
205
205
|
},
|
|
@@ -391,7 +391,7 @@ var init_metadata = __esm({
|
|
|
391
391
|
invocation via @mindstudio-ai/interface. Tokens are published to Redis
|
|
392
392
|
pub/sub as they arrive and forwarded as SSE events on the invoke response.
|
|
393
393
|
The method code itself is unchanged \u2014 streaming is transparent to the
|
|
394
|
-
developer. See
|
|
394
|
+
developer. See Sandboxes/execute.ts and the invoke handler in V2Apps for
|
|
395
395
|
the server-side plumbing.`,
|
|
396
396
|
inputSchema: { "type": "object", "properties": { "message": { "type": "string", "description": "The message to send (prompt for AI, or text for system echo)" }, "source": { "enum": ["user", "system"], "type": "string", "description": 'Message source: "user" sends to AI model, "system" echoes message content directly. Defaults to "user"' }, "modelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "properties": {}, "required": [], "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Model configuration override. Optional; uses the workflow's default model if not specified" }, "structuredOutputType": { "enum": ["text", "json", "csv"], "type": "string", "description": "Output format constraint for structured responses" }, "structuredOutputExample": { "type": "string", "description": "Sample showing the desired output shape (for JSON/CSV formats). A TypeScript interface is also useful here for more complex types." }, "chatHistoryMode": { "enum": ["include", "exclude"], "type": "string", "description": "Whether to include or exclude prior chat history in the AI context" } }, "required": ["message"], "description": "Configuration for the user message step" },
|
|
397
397
|
outputSchema: { "type": "object", "properties": { "content": { "type": "string", "description": "The AI model's response or echoed system message content" } }, "required": ["content"] }
|
|
@@ -1068,7 +1068,7 @@ var init_metadata = __esm({
|
|
|
1068
1068
|
invocation via @mindstudio-ai/interface. Tokens are published to Redis
|
|
1069
1069
|
pub/sub as they arrive and forwarded as SSE events on the invoke response.
|
|
1070
1070
|
The method code itself is unchanged \u2014 streaming is transparent to the
|
|
1071
|
-
developer. See
|
|
1071
|
+
developer. See Sandboxes/execute.ts and the invoke handler in V2Apps for
|
|
1072
1072
|
the server-side plumbing.`,
|
|
1073
1073
|
inputSchema: { "type": "object", "properties": { "message": { "type": "string", "description": "The message to send (prompt for AI, or text for system echo)" }, "source": { "enum": ["user", "system"], "type": "string", "description": 'Message source: "user" sends to AI model, "system" echoes message content directly. Defaults to "user"' }, "modelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "properties": {}, "required": [], "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Model configuration override. Optional; uses the workflow's default model if not specified" }, "structuredOutputType": { "enum": ["text", "json", "csv"], "type": "string", "description": "Output format constraint for structured responses" }, "structuredOutputExample": { "type": "string", "description": "Sample showing the desired output shape (for JSON/CSV formats). A TypeScript interface is also useful here for more complex types." }, "chatHistoryMode": { "enum": ["include", "exclude"], "type": "string", "description": "Whether to include or exclude prior chat history in the AI context" } }, "required": ["message"], "description": "Configuration for the user message step" },
|
|
1074
1074
|
outputSchema: { "type": "object", "properties": { "content": { "type": "string", "description": "The AI model's response or echoed system message content" } }, "required": ["content"] }
|
|
@@ -1213,16 +1213,22 @@ async function requestWithRetry(config, method, url, body, attempt) {
|
|
|
1213
1213
|
const body2 = JSON.parse(text);
|
|
1214
1214
|
details = body2;
|
|
1215
1215
|
const errMsg = (typeof body2.errorMessage === "string" ? body2.errorMessage : void 0) ?? (typeof body2.errorString === "string" ? body2.errorString : void 0) ?? (typeof body2.error === "string" ? body2.error : void 0) ?? (typeof body2.message === "string" ? body2.message : void 0) ?? (typeof body2.details === "string" ? body2.details : void 0);
|
|
1216
|
-
if (errMsg)
|
|
1217
|
-
|
|
1216
|
+
if (errMsg) {
|
|
1217
|
+
message = errMsg;
|
|
1218
|
+
} else if (body2.error || body2.message || body2.details) {
|
|
1218
1219
|
message = JSON.stringify(body2.error ?? body2.message ?? body2.details);
|
|
1219
1220
|
}
|
|
1220
|
-
if (body2.code)
|
|
1221
|
-
|
|
1221
|
+
if (body2.code) {
|
|
1222
|
+
code = String(body2.code);
|
|
1223
|
+
} else if (typeof body2.errorString === "string") {
|
|
1224
|
+
code = body2.errorString;
|
|
1225
|
+
}
|
|
1222
1226
|
} catch {
|
|
1223
1227
|
if (text) {
|
|
1224
1228
|
const stripped = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
1225
|
-
if (stripped)
|
|
1229
|
+
if (stripped) {
|
|
1230
|
+
message = stripped.slice(0, 200);
|
|
1231
|
+
}
|
|
1226
1232
|
}
|
|
1227
1233
|
}
|
|
1228
1234
|
} catch {
|
|
@@ -1600,7 +1606,9 @@ var init_rate_limit = __esm({
|
|
|
1600
1606
|
release() {
|
|
1601
1607
|
this.inflight--;
|
|
1602
1608
|
const next = this.queue.shift();
|
|
1603
|
-
if (next)
|
|
1609
|
+
if (next) {
|
|
1610
|
+
next();
|
|
1611
|
+
}
|
|
1604
1612
|
}
|
|
1605
1613
|
/** Update limits from response headers. */
|
|
1606
1614
|
updateFromHeaders(headers) {
|
|
@@ -1747,7 +1755,9 @@ var init_auth = __esm({
|
|
|
1747
1755
|
{},
|
|
1748
1756
|
{
|
|
1749
1757
|
get(_, prop) {
|
|
1750
|
-
if (typeof prop === "string")
|
|
1758
|
+
if (typeof prop === "string") {
|
|
1759
|
+
return prop;
|
|
1760
|
+
}
|
|
1751
1761
|
return void 0;
|
|
1752
1762
|
}
|
|
1753
1763
|
}
|
|
@@ -1757,9 +1767,15 @@ var init_auth = __esm({
|
|
|
1757
1767
|
|
|
1758
1768
|
// src/db/sql.ts
|
|
1759
1769
|
function serializeParam(val) {
|
|
1760
|
-
if (val === null || val === void 0)
|
|
1761
|
-
|
|
1762
|
-
|
|
1770
|
+
if (val === null || val === void 0) {
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
if (typeof val === "boolean") {
|
|
1774
|
+
return val ? 1 : 0;
|
|
1775
|
+
}
|
|
1776
|
+
if (typeof val === "number" || typeof val === "string") {
|
|
1777
|
+
return val;
|
|
1778
|
+
}
|
|
1763
1779
|
return JSON.stringify(val);
|
|
1764
1780
|
}
|
|
1765
1781
|
function serializeColumnParam(val, columnName, columns) {
|
|
@@ -1770,15 +1786,25 @@ function serializeColumnParam(val, columnName, columns) {
|
|
|
1770
1786
|
return serializeParam(val);
|
|
1771
1787
|
}
|
|
1772
1788
|
function escapeValue(val) {
|
|
1773
|
-
if (val === null || val === void 0)
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
if (typeof val === "
|
|
1789
|
+
if (val === null || val === void 0) {
|
|
1790
|
+
return "NULL";
|
|
1791
|
+
}
|
|
1792
|
+
if (typeof val === "boolean") {
|
|
1793
|
+
return val ? "1" : "0";
|
|
1794
|
+
}
|
|
1795
|
+
if (typeof val === "number") {
|
|
1796
|
+
return String(val);
|
|
1797
|
+
}
|
|
1798
|
+
if (typeof val === "string") {
|
|
1799
|
+
return `'${val.replace(/'/g, "''")}'`;
|
|
1800
|
+
}
|
|
1777
1801
|
const json = JSON.stringify(val);
|
|
1778
1802
|
return `'${json.replace(/'/g, "''")}'`;
|
|
1779
1803
|
}
|
|
1780
1804
|
function deserializeRow(row, columns) {
|
|
1781
|
-
if (row == null)
|
|
1805
|
+
if (row == null) {
|
|
1806
|
+
return row;
|
|
1807
|
+
}
|
|
1782
1808
|
const result = {};
|
|
1783
1809
|
for (const [key, value] of Object.entries(row)) {
|
|
1784
1810
|
const col = columns.find((c) => c.name === key);
|
|
@@ -1812,11 +1838,19 @@ function buildSelect(table, options = {}) {
|
|
|
1812
1838
|
const params = [];
|
|
1813
1839
|
if (options.where) {
|
|
1814
1840
|
sql += ` WHERE ${options.where}`;
|
|
1815
|
-
if (options.whereParams)
|
|
1841
|
+
if (options.whereParams) {
|
|
1842
|
+
params.push(...options.whereParams);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (options.orderBy) {
|
|
1846
|
+
sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
|
|
1847
|
+
}
|
|
1848
|
+
if (options.limit != null) {
|
|
1849
|
+
sql += ` LIMIT ${options.limit}`;
|
|
1850
|
+
}
|
|
1851
|
+
if (options.offset != null) {
|
|
1852
|
+
sql += ` OFFSET ${options.offset}`;
|
|
1816
1853
|
}
|
|
1817
|
-
if (options.orderBy) sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
|
|
1818
|
-
if (options.limit != null) sql += ` LIMIT ${options.limit}`;
|
|
1819
|
-
if (options.offset != null) sql += ` OFFSET ${options.offset}`;
|
|
1820
1854
|
return { sql, params: params.length > 0 ? params : void 0 };
|
|
1821
1855
|
}
|
|
1822
1856
|
function assertSqlIdentifier(name, context) {
|
|
@@ -1845,27 +1879,47 @@ function aggregateExpr(fn, column) {
|
|
|
1845
1879
|
}
|
|
1846
1880
|
}
|
|
1847
1881
|
function buildScalarAggregate(table, fn, column, where, whereParams) {
|
|
1848
|
-
if (column != null)
|
|
1882
|
+
if (column != null) {
|
|
1883
|
+
assertSqlIdentifier(column, "aggregate column");
|
|
1884
|
+
}
|
|
1849
1885
|
let sql = `SELECT ${aggregateExpr(fn, column ?? void 0)} AS value FROM ${table}`;
|
|
1850
|
-
if (where)
|
|
1886
|
+
if (where) {
|
|
1887
|
+
sql += ` WHERE ${where}`;
|
|
1888
|
+
}
|
|
1851
1889
|
return { sql, params: whereParams?.length ? whereParams : void 0 };
|
|
1852
1890
|
}
|
|
1853
1891
|
function buildGroupedAggregate(table, options) {
|
|
1854
|
-
for (const col of options.by)
|
|
1892
|
+
for (const col of options.by) {
|
|
1893
|
+
assertSqlIdentifier(col, "group-by column");
|
|
1894
|
+
}
|
|
1855
1895
|
for (const term of options.select) {
|
|
1856
1896
|
assertSqlIdentifier(term.alias, "aggregate alias");
|
|
1857
|
-
if (term.column != null)
|
|
1897
|
+
if (term.column != null) {
|
|
1898
|
+
assertSqlIdentifier(term.column, "aggregate column");
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (options.orderBy != null) {
|
|
1902
|
+
assertSqlIdentifier(options.orderBy, "aggregate order key");
|
|
1858
1903
|
}
|
|
1859
|
-
if (options.orderBy != null) assertSqlIdentifier(options.orderBy, "aggregate order key");
|
|
1860
1904
|
const selectList = [
|
|
1861
1905
|
...options.by,
|
|
1862
|
-
...options.select.map(
|
|
1906
|
+
...options.select.map(
|
|
1907
|
+
(t) => `${aggregateExpr(t.fn, t.column)} AS ${t.alias}`
|
|
1908
|
+
)
|
|
1863
1909
|
].join(", ");
|
|
1864
1910
|
let sql = `SELECT ${selectList} FROM ${table}`;
|
|
1865
|
-
if (options.where)
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
if (options.
|
|
1911
|
+
if (options.where) {
|
|
1912
|
+
sql += ` WHERE ${options.where}`;
|
|
1913
|
+
}
|
|
1914
|
+
if (options.by.length > 0) {
|
|
1915
|
+
sql += ` GROUP BY ${options.by.join(", ")}`;
|
|
1916
|
+
}
|
|
1917
|
+
if (options.orderBy) {
|
|
1918
|
+
sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
|
|
1919
|
+
}
|
|
1920
|
+
if (options.limit != null) {
|
|
1921
|
+
sql += ` LIMIT ${options.limit}`;
|
|
1922
|
+
}
|
|
1869
1923
|
return {
|
|
1870
1924
|
sql,
|
|
1871
1925
|
params: options.whereParams?.length ? options.whereParams : void 0
|
|
@@ -1874,7 +1928,10 @@ function buildGroupedAggregate(table, options) {
|
|
|
1874
1928
|
function buildExists(table, where, whereParams, negate) {
|
|
1875
1929
|
const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
|
|
1876
1930
|
const fn = negate ? "NOT EXISTS" : "EXISTS";
|
|
1877
|
-
return {
|
|
1931
|
+
return {
|
|
1932
|
+
sql: `SELECT ${fn}(${inner}) as result`,
|
|
1933
|
+
params: whereParams?.length ? whereParams : void 0
|
|
1934
|
+
};
|
|
1878
1935
|
}
|
|
1879
1936
|
function buildInsert(table, data, columns) {
|
|
1880
1937
|
const filtered = stripSystemColumns(data);
|
|
@@ -1904,9 +1961,7 @@ function buildUpsert(table, data, conflictColumns, columns) {
|
|
|
1904
1961
|
const filtered = stripSystemColumns(data);
|
|
1905
1962
|
const keys = Object.keys(filtered);
|
|
1906
1963
|
const placeholders = keys.map(() => "?").join(", ");
|
|
1907
|
-
const params = keys.map(
|
|
1908
|
-
(k) => serializeColumnParam(filtered[k], k, columns)
|
|
1909
|
-
);
|
|
1964
|
+
const params = keys.map((k) => serializeColumnParam(filtered[k], k, columns));
|
|
1910
1965
|
const updateKeys = keys.filter((k) => !conflictColumns.includes(k));
|
|
1911
1966
|
const conflict = conflictColumns.join(", ");
|
|
1912
1967
|
const sql = updateKeys.length > 0 ? `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflict}) DO UPDATE SET ${updateKeys.map((k) => `${k} = excluded.${k}`).join(", ")} RETURNING *` : `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflict}) DO NOTHING RETURNING *`;
|
|
@@ -1914,7 +1969,9 @@ function buildUpsert(table, data, conflictColumns, columns) {
|
|
|
1914
1969
|
}
|
|
1915
1970
|
function buildDelete(table, where, whereParams) {
|
|
1916
1971
|
let sql = `DELETE FROM ${table}`;
|
|
1917
|
-
if (where)
|
|
1972
|
+
if (where) {
|
|
1973
|
+
sql += ` WHERE ${where}`;
|
|
1974
|
+
}
|
|
1918
1975
|
return { sql, params: whereParams?.length ? whereParams : void 0 };
|
|
1919
1976
|
}
|
|
1920
1977
|
function stripSystemColumns(data) {
|
|
@@ -1950,37 +2007,65 @@ function compilePredicate(fn, bindings) {
|
|
|
1950
2007
|
try {
|
|
1951
2008
|
const source = fn.toString();
|
|
1952
2009
|
const names = extractParamNames(source);
|
|
1953
|
-
if (!names)
|
|
2010
|
+
if (!names) {
|
|
2011
|
+
return { type: "js", fn, reason: "could not extract parameter name" };
|
|
2012
|
+
}
|
|
1954
2013
|
const body = extractBody(source);
|
|
1955
|
-
if (!body)
|
|
2014
|
+
if (!body) {
|
|
2015
|
+
return { type: "js", fn, reason: "could not extract function body" };
|
|
2016
|
+
}
|
|
1956
2017
|
const tokens = tokenize(body);
|
|
1957
|
-
if (tokens.length === 0)
|
|
2018
|
+
if (tokens.length === 0) {
|
|
2019
|
+
return { type: "js", fn, reason: "empty token stream" };
|
|
2020
|
+
}
|
|
1958
2021
|
const parser = new Parser(tokens, names.row, names.bindings, bindings, fn);
|
|
1959
2022
|
const ast = parser.parseExpression();
|
|
1960
|
-
if (!ast)
|
|
1961
|
-
|
|
2023
|
+
if (!ast) {
|
|
2024
|
+
return { type: "js", fn, reason: "could not parse expression" };
|
|
2025
|
+
}
|
|
2026
|
+
if (parser.pos < tokens.length) {
|
|
2027
|
+
return { type: "js", fn, reason: "unexpected tokens after expression" };
|
|
2028
|
+
}
|
|
1962
2029
|
const where = compileNode(ast);
|
|
1963
|
-
if (!where)
|
|
2030
|
+
if (!where) {
|
|
2031
|
+
return { type: "js", fn, reason: "could not compile to SQL" };
|
|
2032
|
+
}
|
|
1964
2033
|
return { type: "sql", where };
|
|
1965
2034
|
} catch (err) {
|
|
1966
|
-
return {
|
|
2035
|
+
return {
|
|
2036
|
+
type: "js",
|
|
2037
|
+
fn,
|
|
2038
|
+
reason: `compilation error: ${err?.message || "unknown"}`
|
|
2039
|
+
};
|
|
1967
2040
|
}
|
|
1968
2041
|
}
|
|
1969
2042
|
function extractParamNames(source) {
|
|
1970
2043
|
const arrowIdx = source.indexOf("=>");
|
|
1971
|
-
if (arrowIdx === -1)
|
|
2044
|
+
if (arrowIdx === -1) {
|
|
2045
|
+
return null;
|
|
2046
|
+
}
|
|
1972
2047
|
let paramList = source.slice(0, arrowIdx).trim();
|
|
1973
2048
|
if (paramList.startsWith("(") && paramList.endsWith(")")) {
|
|
1974
2049
|
paramList = paramList.slice(1, -1).trim();
|
|
1975
2050
|
}
|
|
1976
|
-
if (paramList.length === 0)
|
|
2051
|
+
if (paramList.length === 0) {
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
1977
2054
|
const parts = splitParams(paramList);
|
|
1978
|
-
if (parts.length === 0 || parts.length > 2)
|
|
2055
|
+
if (parts.length === 0 || parts.length > 2) {
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
1979
2058
|
const row = stripTypeAnnotation(parts[0]);
|
|
1980
|
-
if (!row)
|
|
1981
|
-
|
|
2059
|
+
if (!row) {
|
|
2060
|
+
return null;
|
|
2061
|
+
}
|
|
2062
|
+
if (parts.length === 1) {
|
|
2063
|
+
return { row };
|
|
2064
|
+
}
|
|
1982
2065
|
const bindings = stripTypeAnnotation(parts[1]);
|
|
1983
|
-
if (!bindings)
|
|
2066
|
+
if (!bindings) {
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
1984
2069
|
return { row, bindings };
|
|
1985
2070
|
}
|
|
1986
2071
|
function splitParams(input) {
|
|
@@ -1988,8 +2073,11 @@ function splitParams(input) {
|
|
|
1988
2073
|
let depth = 0;
|
|
1989
2074
|
let current = "";
|
|
1990
2075
|
for (const ch of input) {
|
|
1991
|
-
if (ch === "<" || ch === "(")
|
|
1992
|
-
|
|
2076
|
+
if (ch === "<" || ch === "(") {
|
|
2077
|
+
depth++;
|
|
2078
|
+
} else if (ch === ">" || ch === ")") {
|
|
2079
|
+
depth--;
|
|
2080
|
+
}
|
|
1993
2081
|
if (ch === "," && depth === 0) {
|
|
1994
2082
|
parts.push(current.trim());
|
|
1995
2083
|
current = "";
|
|
@@ -1997,7 +2085,9 @@ function splitParams(input) {
|
|
|
1997
2085
|
current += ch;
|
|
1998
2086
|
}
|
|
1999
2087
|
}
|
|
2000
|
-
if (current.trim())
|
|
2088
|
+
if (current.trim()) {
|
|
2089
|
+
parts.push(current.trim());
|
|
2090
|
+
}
|
|
2001
2091
|
return parts;
|
|
2002
2092
|
}
|
|
2003
2093
|
function stripTypeAnnotation(part) {
|
|
@@ -2007,11 +2097,15 @@ function stripTypeAnnotation(part) {
|
|
|
2007
2097
|
}
|
|
2008
2098
|
function extractBody(source) {
|
|
2009
2099
|
const arrowIdx = source.indexOf("=>");
|
|
2010
|
-
if (arrowIdx === -1)
|
|
2100
|
+
if (arrowIdx === -1) {
|
|
2101
|
+
return null;
|
|
2102
|
+
}
|
|
2011
2103
|
let body = source.slice(arrowIdx + 2).trim();
|
|
2012
2104
|
if (body.startsWith("{")) {
|
|
2013
2105
|
const match = body.match(/^\{\s*return\s+([\s\S]+?)\s*;?\s*\}$/);
|
|
2014
|
-
if (!match)
|
|
2106
|
+
if (!match) {
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2015
2109
|
body = match[1];
|
|
2016
2110
|
}
|
|
2017
2111
|
return body.trim() || null;
|
|
@@ -2032,18 +2126,24 @@ function tokenize(expr) {
|
|
|
2032
2126
|
while (i < expr.length && expr[i] !== quote) {
|
|
2033
2127
|
if (expr[i] === "\\") {
|
|
2034
2128
|
i++;
|
|
2035
|
-
if (i < expr.length)
|
|
2129
|
+
if (i < expr.length) {
|
|
2130
|
+
str += expr[i];
|
|
2131
|
+
}
|
|
2036
2132
|
} else {
|
|
2037
2133
|
str += expr[i];
|
|
2038
2134
|
}
|
|
2039
2135
|
i++;
|
|
2040
2136
|
}
|
|
2041
|
-
if (i >= expr.length)
|
|
2137
|
+
if (i >= expr.length) {
|
|
2138
|
+
return [];
|
|
2139
|
+
}
|
|
2042
2140
|
i++;
|
|
2043
2141
|
tokens.push({ type: "string", value: str });
|
|
2044
2142
|
continue;
|
|
2045
2143
|
}
|
|
2046
|
-
if (ch === "`")
|
|
2144
|
+
if (ch === "`") {
|
|
2145
|
+
return [];
|
|
2146
|
+
}
|
|
2047
2147
|
if (/[0-9]/.test(ch) || ch === "-" && i + 1 < expr.length && /[0-9]/.test(expr[i + 1])) {
|
|
2048
2148
|
let num = ch;
|
|
2049
2149
|
i++;
|
|
@@ -2120,7 +2220,9 @@ function compileNode(node) {
|
|
|
2120
2220
|
case "nullCheck":
|
|
2121
2221
|
return `${node.field} ${node.isNull ? "IS NULL" : "IS NOT NULL"}`;
|
|
2122
2222
|
case "in": {
|
|
2123
|
-
if (node.values.length === 0)
|
|
2223
|
+
if (node.values.length === 0) {
|
|
2224
|
+
return "0";
|
|
2225
|
+
}
|
|
2124
2226
|
const vals = node.values.map(escapeValue).join(", ");
|
|
2125
2227
|
return `${node.field} IN (${vals})`;
|
|
2126
2228
|
}
|
|
@@ -2131,12 +2233,16 @@ function compileNode(node) {
|
|
|
2131
2233
|
case "logical": {
|
|
2132
2234
|
const left = compileNode(node.left);
|
|
2133
2235
|
const right = compileNode(node.right);
|
|
2134
|
-
if (!left || !right)
|
|
2236
|
+
if (!left || !right) {
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2135
2239
|
return `(${left} ${node.operator} ${right})`;
|
|
2136
2240
|
}
|
|
2137
2241
|
case "not": {
|
|
2138
2242
|
const inner = compileNode(node.operand);
|
|
2139
|
-
if (!inner)
|
|
2243
|
+
if (!inner) {
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2140
2246
|
return `NOT (${inner})`;
|
|
2141
2247
|
}
|
|
2142
2248
|
default:
|
|
@@ -2171,9 +2277,15 @@ var init_predicate = __esm({
|
|
|
2171
2277
|
/** Check if the current token matches an expected type and value. */
|
|
2172
2278
|
match(type, value) {
|
|
2173
2279
|
const t = this.peek();
|
|
2174
|
-
if (!t)
|
|
2175
|
-
|
|
2176
|
-
|
|
2280
|
+
if (!t) {
|
|
2281
|
+
return false;
|
|
2282
|
+
}
|
|
2283
|
+
if (t.type !== type) {
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
if (value !== void 0 && t.value !== value) {
|
|
2287
|
+
return false;
|
|
2288
|
+
}
|
|
2177
2289
|
return true;
|
|
2178
2290
|
}
|
|
2179
2291
|
/** Consume a token if it matches, otherwise return false. */
|
|
@@ -2192,11 +2304,15 @@ var init_predicate = __esm({
|
|
|
2192
2304
|
/** or_expr → and_expr ( '||' and_expr )* */
|
|
2193
2305
|
parseOr() {
|
|
2194
2306
|
let left = this.parseAnd();
|
|
2195
|
-
if (!left)
|
|
2307
|
+
if (!left) {
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2196
2310
|
while (this.match("operator", "||")) {
|
|
2197
2311
|
this.advance();
|
|
2198
2312
|
const right = this.parseAnd();
|
|
2199
|
-
if (!right)
|
|
2313
|
+
if (!right) {
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2200
2316
|
left = { kind: "logical", operator: "OR", left, right };
|
|
2201
2317
|
}
|
|
2202
2318
|
return left;
|
|
@@ -2204,11 +2320,15 @@ var init_predicate = __esm({
|
|
|
2204
2320
|
/** and_expr → not_expr ( '&&' not_expr )* */
|
|
2205
2321
|
parseAnd() {
|
|
2206
2322
|
let left = this.parseNot();
|
|
2207
|
-
if (!left)
|
|
2323
|
+
if (!left) {
|
|
2324
|
+
return null;
|
|
2325
|
+
}
|
|
2208
2326
|
while (this.match("operator", "&&")) {
|
|
2209
2327
|
this.advance();
|
|
2210
2328
|
const right = this.parseNot();
|
|
2211
|
-
if (!right)
|
|
2329
|
+
if (!right) {
|
|
2330
|
+
return null;
|
|
2331
|
+
}
|
|
2212
2332
|
left = { kind: "logical", operator: "AND", left, right };
|
|
2213
2333
|
}
|
|
2214
2334
|
return left;
|
|
@@ -2220,12 +2340,18 @@ var init_predicate = __esm({
|
|
|
2220
2340
|
if (this.match("lparen")) {
|
|
2221
2341
|
this.advance();
|
|
2222
2342
|
const inner2 = this.parseExpression();
|
|
2223
|
-
if (!inner2)
|
|
2224
|
-
|
|
2343
|
+
if (!inner2) {
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
if (!this.eat("rparen")) {
|
|
2347
|
+
return null;
|
|
2348
|
+
}
|
|
2225
2349
|
return { kind: "not", operand: inner2 };
|
|
2226
2350
|
}
|
|
2227
2351
|
const inner = this.parsePrimary();
|
|
2228
|
-
if (!inner)
|
|
2352
|
+
if (!inner) {
|
|
2353
|
+
return null;
|
|
2354
|
+
}
|
|
2229
2355
|
if (inner.kind === "booleanField") {
|
|
2230
2356
|
return { ...inner, negated: !inner.negated };
|
|
2231
2357
|
}
|
|
@@ -2243,8 +2369,12 @@ var init_predicate = __esm({
|
|
|
2243
2369
|
if (this.match("lparen")) {
|
|
2244
2370
|
this.advance();
|
|
2245
2371
|
const inner = this.parseExpression();
|
|
2246
|
-
if (!inner)
|
|
2247
|
-
|
|
2372
|
+
if (!inner) {
|
|
2373
|
+
return null;
|
|
2374
|
+
}
|
|
2375
|
+
if (!this.eat("rparen")) {
|
|
2376
|
+
return null;
|
|
2377
|
+
}
|
|
2248
2378
|
return inner;
|
|
2249
2379
|
}
|
|
2250
2380
|
if (this.match("lbracket")) {
|
|
@@ -2273,7 +2403,9 @@ var init_predicate = __esm({
|
|
|
2273
2403
|
parseFieldExpression() {
|
|
2274
2404
|
this.advance();
|
|
2275
2405
|
const field = this.parseFieldPath();
|
|
2276
|
-
if (!field)
|
|
2406
|
+
if (!field) {
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2277
2409
|
const next = this.peek();
|
|
2278
2410
|
if (next?.type === "dot" && this.lookAheadForIncludes()) {
|
|
2279
2411
|
return this.parseFieldIncludes(field);
|
|
@@ -2288,11 +2420,17 @@ var init_predicate = __esm({
|
|
|
2288
2420
|
* `o.status` → `"status"`, `o.address.city` → `"json_extract(address, '$.city')"`.
|
|
2289
2421
|
*/
|
|
2290
2422
|
parseFieldPath() {
|
|
2291
|
-
if (!this.eat("dot"))
|
|
2292
|
-
|
|
2423
|
+
if (!this.eat("dot")) {
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
if (!this.match("identifier")) {
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2293
2429
|
const parts = [this.advance().value];
|
|
2294
2430
|
while (this.match("dot") && this.tokens[this.pos + 1]?.type === "identifier") {
|
|
2295
|
-
if (this.tokens[this.pos + 2]?.type === "lparen")
|
|
2431
|
+
if (this.tokens[this.pos + 2]?.type === "lparen") {
|
|
2432
|
+
break;
|
|
2433
|
+
}
|
|
2296
2434
|
this.advance();
|
|
2297
2435
|
parts.push(this.advance().value);
|
|
2298
2436
|
}
|
|
@@ -2311,7 +2449,9 @@ var init_predicate = __esm({
|
|
|
2311
2449
|
const opToken = this.advance();
|
|
2312
2450
|
const jsOp = opToken.value;
|
|
2313
2451
|
const value = this.parseValue();
|
|
2314
|
-
if (value === PARSE_FAILED)
|
|
2452
|
+
if (value === PARSE_FAILED) {
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2315
2455
|
if (value === null || value === void 0) {
|
|
2316
2456
|
if (jsOp === "===" || jsOp === "==") {
|
|
2317
2457
|
return { kind: "nullCheck", field, isNull: true };
|
|
@@ -2322,7 +2462,9 @@ var init_predicate = __esm({
|
|
|
2322
2462
|
return null;
|
|
2323
2463
|
}
|
|
2324
2464
|
const sqlOp = JS_TO_SQL_OP[jsOp];
|
|
2325
|
-
if (!sqlOp)
|
|
2465
|
+
if (!sqlOp) {
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2326
2468
|
return { kind: "comparison", field, operator: sqlOp, value };
|
|
2327
2469
|
}
|
|
2328
2470
|
/**
|
|
@@ -2332,10 +2474,16 @@ var init_predicate = __esm({
|
|
|
2332
2474
|
parseFieldIncludes(field) {
|
|
2333
2475
|
this.advance();
|
|
2334
2476
|
this.advance();
|
|
2335
|
-
if (!this.eat("lparen"))
|
|
2477
|
+
if (!this.eat("lparen")) {
|
|
2478
|
+
return null;
|
|
2479
|
+
}
|
|
2336
2480
|
const value = this.parseValue();
|
|
2337
|
-
if (value === PARSE_FAILED || typeof value !== "string")
|
|
2338
|
-
|
|
2481
|
+
if (value === PARSE_FAILED || typeof value !== "string") {
|
|
2482
|
+
return null;
|
|
2483
|
+
}
|
|
2484
|
+
if (!this.eat("rparen")) {
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2339
2487
|
const escaped = value.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
2340
2488
|
return { kind: "like", field, pattern: `%${escaped}%` };
|
|
2341
2489
|
}
|
|
@@ -2348,22 +2496,38 @@ var init_predicate = __esm({
|
|
|
2348
2496
|
const values = [];
|
|
2349
2497
|
while (!this.match("rbracket")) {
|
|
2350
2498
|
if (values.length > 0) {
|
|
2351
|
-
if (!this.eat("comma"))
|
|
2499
|
+
if (!this.eat("comma")) {
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2352
2502
|
}
|
|
2353
2503
|
const val = this.parseValue();
|
|
2354
|
-
if (val === PARSE_FAILED)
|
|
2504
|
+
if (val === PARSE_FAILED) {
|
|
2505
|
+
return null;
|
|
2506
|
+
}
|
|
2355
2507
|
values.push(val);
|
|
2356
2508
|
}
|
|
2357
2509
|
this.advance();
|
|
2358
|
-
if (!this.eat("dot"))
|
|
2359
|
-
|
|
2510
|
+
if (!this.eat("dot")) {
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
if (!this.match("identifier", "includes")) {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2360
2516
|
this.advance();
|
|
2361
|
-
if (!this.eat("lparen"))
|
|
2362
|
-
|
|
2517
|
+
if (!this.eat("lparen")) {
|
|
2518
|
+
return null;
|
|
2519
|
+
}
|
|
2520
|
+
if (!this.match("identifier", this.paramName)) {
|
|
2521
|
+
return null;
|
|
2522
|
+
}
|
|
2363
2523
|
this.advance();
|
|
2364
2524
|
const field = this.parseFieldPath();
|
|
2365
|
-
if (!field)
|
|
2366
|
-
|
|
2525
|
+
if (!field) {
|
|
2526
|
+
return null;
|
|
2527
|
+
}
|
|
2528
|
+
if (!this.eat("rparen")) {
|
|
2529
|
+
return null;
|
|
2530
|
+
}
|
|
2367
2531
|
return { kind: "in", field, values };
|
|
2368
2532
|
}
|
|
2369
2533
|
/**
|
|
@@ -2374,7 +2538,9 @@ var init_predicate = __esm({
|
|
|
2374
2538
|
*/
|
|
2375
2539
|
parseNonParamExpression() {
|
|
2376
2540
|
const ident = this.peek().value;
|
|
2377
|
-
if (ident === "true" || ident === "false")
|
|
2541
|
+
if (ident === "true" || ident === "false") {
|
|
2542
|
+
return null;
|
|
2543
|
+
}
|
|
2378
2544
|
return null;
|
|
2379
2545
|
}
|
|
2380
2546
|
/**
|
|
@@ -2386,17 +2552,33 @@ var init_predicate = __esm({
|
|
|
2386
2552
|
*/
|
|
2387
2553
|
parseBindingsArrayIncludes() {
|
|
2388
2554
|
const bound = this.tryResolveBindingsValue();
|
|
2389
|
-
if (bound === PARSE_FAILED)
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
if (!
|
|
2555
|
+
if (bound === PARSE_FAILED) {
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2558
|
+
if (!Array.isArray(bound)) {
|
|
2559
|
+
return null;
|
|
2560
|
+
}
|
|
2561
|
+
if (!this.eat("dot")) {
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
if (!this.match("identifier", "includes")) {
|
|
2565
|
+
return null;
|
|
2566
|
+
}
|
|
2393
2567
|
this.advance();
|
|
2394
|
-
if (!this.eat("lparen"))
|
|
2395
|
-
|
|
2568
|
+
if (!this.eat("lparen")) {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2571
|
+
if (!this.match("identifier", this.paramName)) {
|
|
2572
|
+
return null;
|
|
2573
|
+
}
|
|
2396
2574
|
this.advance();
|
|
2397
2575
|
const field = this.parseFieldPath();
|
|
2398
|
-
if (!field)
|
|
2399
|
-
|
|
2576
|
+
if (!field) {
|
|
2577
|
+
return null;
|
|
2578
|
+
}
|
|
2579
|
+
if (!this.eat("rparen")) {
|
|
2580
|
+
return null;
|
|
2581
|
+
}
|
|
2400
2582
|
return { kind: "in", field, values: bound };
|
|
2401
2583
|
}
|
|
2402
2584
|
/**
|
|
@@ -2410,14 +2592,22 @@ var init_predicate = __esm({
|
|
|
2410
2592
|
* Restores `pos` on failure so callers can fall through cleanly.
|
|
2411
2593
|
*/
|
|
2412
2594
|
tryResolveBindingsValue() {
|
|
2413
|
-
if (!this.bindingsName)
|
|
2414
|
-
|
|
2415
|
-
|
|
2595
|
+
if (!this.bindingsName) {
|
|
2596
|
+
return PARSE_FAILED;
|
|
2597
|
+
}
|
|
2598
|
+
if (!this.match("identifier", this.bindingsName)) {
|
|
2599
|
+
return PARSE_FAILED;
|
|
2600
|
+
}
|
|
2601
|
+
if (this.bindingsValue == null) {
|
|
2602
|
+
return PARSE_FAILED;
|
|
2603
|
+
}
|
|
2416
2604
|
const startPos = this.pos;
|
|
2417
2605
|
this.advance();
|
|
2418
2606
|
const path = [];
|
|
2419
2607
|
while (this.match("dot") && this.tokens[this.pos + 1]?.type === "identifier") {
|
|
2420
|
-
if (this.tokens[this.pos + 2]?.type === "lparen")
|
|
2608
|
+
if (this.tokens[this.pos + 2]?.type === "lparen") {
|
|
2609
|
+
break;
|
|
2610
|
+
}
|
|
2421
2611
|
this.advance();
|
|
2422
2612
|
path.push(this.advance().value);
|
|
2423
2613
|
}
|
|
@@ -2447,7 +2637,9 @@ var init_predicate = __esm({
|
|
|
2447
2637
|
*/
|
|
2448
2638
|
parseValue() {
|
|
2449
2639
|
const t = this.peek();
|
|
2450
|
-
if (!t)
|
|
2640
|
+
if (!t) {
|
|
2641
|
+
return PARSE_FAILED;
|
|
2642
|
+
}
|
|
2451
2643
|
if (t.type === "string") {
|
|
2452
2644
|
this.advance();
|
|
2453
2645
|
return t.value;
|
|
@@ -2475,7 +2667,9 @@ var init_predicate = __esm({
|
|
|
2475
2667
|
}
|
|
2476
2668
|
if (this.bindingsName && t.value === this.bindingsName) {
|
|
2477
2669
|
const bound = this.tryResolveBindingsValue();
|
|
2478
|
-
if (bound !== PARSE_FAILED)
|
|
2670
|
+
if (bound !== PARSE_FAILED) {
|
|
2671
|
+
return bound;
|
|
2672
|
+
}
|
|
2479
2673
|
}
|
|
2480
2674
|
return this.resolveClosureVariable();
|
|
2481
2675
|
}
|
|
@@ -2644,11 +2838,21 @@ function aggregateValues(fn, values) {
|
|
|
2644
2838
|
}
|
|
2645
2839
|
}
|
|
2646
2840
|
function compareForOrder(a, b) {
|
|
2647
|
-
if (a == null && b == null)
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
if (a
|
|
2651
|
-
|
|
2841
|
+
if (a == null && b == null) {
|
|
2842
|
+
return 0;
|
|
2843
|
+
}
|
|
2844
|
+
if (a == null) {
|
|
2845
|
+
return -1;
|
|
2846
|
+
}
|
|
2847
|
+
if (b == null) {
|
|
2848
|
+
return 1;
|
|
2849
|
+
}
|
|
2850
|
+
if (a < b) {
|
|
2851
|
+
return -1;
|
|
2852
|
+
}
|
|
2853
|
+
if (a > b) {
|
|
2854
|
+
return 1;
|
|
2855
|
+
}
|
|
2652
2856
|
return 0;
|
|
2653
2857
|
}
|
|
2654
2858
|
function computeAggregateInJs(rows, spec) {
|
|
@@ -2656,8 +2860,11 @@ function computeAggregateInJs(rows, spec) {
|
|
|
2656
2860
|
for (const row of rows) {
|
|
2657
2861
|
const key = JSON.stringify(spec.by.map((col) => row[col]));
|
|
2658
2862
|
const group = groups.get(key);
|
|
2659
|
-
if (group)
|
|
2660
|
-
|
|
2863
|
+
if (group) {
|
|
2864
|
+
group.push(row);
|
|
2865
|
+
} else {
|
|
2866
|
+
groups.set(key, [row]);
|
|
2867
|
+
}
|
|
2661
2868
|
}
|
|
2662
2869
|
if (spec.by.length === 0 && groups.size === 0) {
|
|
2663
2870
|
groups.set("[]", []);
|
|
@@ -2678,7 +2885,9 @@ function computeAggregateInJs(rows, spec) {
|
|
|
2678
2885
|
if (spec.orderBy) {
|
|
2679
2886
|
const key = spec.orderBy;
|
|
2680
2887
|
out.sort((a, b) => compareForOrder(a[key], b[key]));
|
|
2681
|
-
if (spec.desc)
|
|
2888
|
+
if (spec.desc) {
|
|
2889
|
+
out.reverse();
|
|
2890
|
+
}
|
|
2682
2891
|
}
|
|
2683
2892
|
if (spec.limit != null) {
|
|
2684
2893
|
out = out.slice(0, spec.limit);
|
|
@@ -2833,7 +3042,11 @@ var init_query = __esm({
|
|
|
2833
3042
|
*/
|
|
2834
3043
|
sum(accessor) {
|
|
2835
3044
|
return this._clone({
|
|
2836
|
-
aggregate: {
|
|
3045
|
+
aggregate: {
|
|
3046
|
+
kind: "scalar",
|
|
3047
|
+
fn: "sum",
|
|
3048
|
+
accessor
|
|
3049
|
+
},
|
|
2837
3050
|
postProcess: (rows) => rows.reduce((total, row) => {
|
|
2838
3051
|
const value = accessor(row);
|
|
2839
3052
|
return value == null ? total : total + value;
|
|
@@ -2847,7 +3060,11 @@ var init_query = __esm({
|
|
|
2847
3060
|
*/
|
|
2848
3061
|
avg(accessor) {
|
|
2849
3062
|
return this._clone({
|
|
2850
|
-
aggregate: {
|
|
3063
|
+
aggregate: {
|
|
3064
|
+
kind: "scalar",
|
|
3065
|
+
fn: "avg",
|
|
3066
|
+
accessor
|
|
3067
|
+
},
|
|
2851
3068
|
postProcess: (rows) => {
|
|
2852
3069
|
const values = rows.map((row) => accessor(row)).filter((v) => v != null);
|
|
2853
3070
|
return values.length === 0 ? null : values.reduce((s, v) => s + v, 0) / values.length;
|
|
@@ -2870,10 +3087,7 @@ var init_query = __esm({
|
|
|
2870
3087
|
);
|
|
2871
3088
|
return this._clone({
|
|
2872
3089
|
aggregate: { kind: "grouped", spec: normalized },
|
|
2873
|
-
postProcess: (rows) => computeAggregateInJs(
|
|
2874
|
-
rows,
|
|
2875
|
-
normalized
|
|
2876
|
-
)
|
|
3090
|
+
postProcess: (rows) => computeAggregateInJs(rows, normalized)
|
|
2877
3091
|
});
|
|
2878
3092
|
}
|
|
2879
3093
|
some() {
|
|
@@ -2895,7 +3109,9 @@ var init_query = __esm({
|
|
|
2895
3109
|
const row = results[0]?.rows[0];
|
|
2896
3110
|
return row?.result === 1;
|
|
2897
3111
|
}
|
|
2898
|
-
if (this._predicates.length === 0)
|
|
3112
|
+
if (this._predicates.length === 0) {
|
|
3113
|
+
return true;
|
|
3114
|
+
}
|
|
2899
3115
|
const allRows = await this._fetchAllRows();
|
|
2900
3116
|
return allRows.every(
|
|
2901
3117
|
(row) => this._predicates.every((p) => p.fn(row, p.bindings))
|
|
@@ -2914,8 +3130,11 @@ var init_query = __esm({
|
|
|
2914
3130
|
for (const row of rows) {
|
|
2915
3131
|
const key = accessor(row);
|
|
2916
3132
|
const group = map.get(key);
|
|
2917
|
-
if (group)
|
|
2918
|
-
|
|
3133
|
+
if (group) {
|
|
3134
|
+
group.push(row);
|
|
3135
|
+
} else {
|
|
3136
|
+
map.set(key, [row]);
|
|
3137
|
+
}
|
|
2919
3138
|
}
|
|
2920
3139
|
return map;
|
|
2921
3140
|
}
|
|
@@ -2941,14 +3160,26 @@ var init_query = __esm({
|
|
|
2941
3160
|
limit: this._limit,
|
|
2942
3161
|
offset: this._offset
|
|
2943
3162
|
});
|
|
2944
|
-
return {
|
|
3163
|
+
return {
|
|
3164
|
+
type: "query",
|
|
3165
|
+
query,
|
|
3166
|
+
fallbackQuery: null,
|
|
3167
|
+
config: this._config,
|
|
3168
|
+
postProcess: this._postProcess
|
|
3169
|
+
};
|
|
2945
3170
|
}
|
|
2946
3171
|
const compiled = this._compilePredicates();
|
|
2947
3172
|
const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
|
|
2948
3173
|
if (compiled.allSql) {
|
|
2949
3174
|
const agg = this._compileAggregateSql(compiled.sqlWhere);
|
|
2950
3175
|
if (agg) {
|
|
2951
|
-
return {
|
|
3176
|
+
return {
|
|
3177
|
+
type: "query",
|
|
3178
|
+
query: agg.query,
|
|
3179
|
+
fallbackQuery: null,
|
|
3180
|
+
config: this._config,
|
|
3181
|
+
processRaw: agg.processRaw
|
|
3182
|
+
};
|
|
2952
3183
|
}
|
|
2953
3184
|
const query = buildSelect(this._config.tableName, {
|
|
2954
3185
|
where: compiled.sqlWhere || void 0,
|
|
@@ -2957,7 +3188,13 @@ var init_query = __esm({
|
|
|
2957
3188
|
limit: this._limit,
|
|
2958
3189
|
offset: this._offset
|
|
2959
3190
|
});
|
|
2960
|
-
return {
|
|
3191
|
+
return {
|
|
3192
|
+
type: "query",
|
|
3193
|
+
query,
|
|
3194
|
+
fallbackQuery: null,
|
|
3195
|
+
config: this._config,
|
|
3196
|
+
postProcess: this._postProcess
|
|
3197
|
+
};
|
|
2961
3198
|
}
|
|
2962
3199
|
const fallbackQuery = buildSelect(this._config.tableName);
|
|
2963
3200
|
return {
|
|
@@ -2993,15 +3230,21 @@ var init_query = __esm({
|
|
|
2993
3230
|
if (compiled.query) {
|
|
2994
3231
|
return compiled.postProcess ? compiled.postProcess(rows) : rows;
|
|
2995
3232
|
}
|
|
2996
|
-
let filtered = compiled.predicates ? rows.filter(
|
|
3233
|
+
let filtered = compiled.predicates ? rows.filter(
|
|
3234
|
+
(row) => compiled.predicates.every((p) => p.fn(row, p.bindings))
|
|
3235
|
+
) : rows;
|
|
2997
3236
|
if (compiled.sortAccessor) {
|
|
2998
3237
|
const accessor = compiled.sortAccessor;
|
|
2999
3238
|
const reversed = compiled.reversed ?? false;
|
|
3000
3239
|
filtered.sort((a, b) => {
|
|
3001
3240
|
const aVal = accessor(a);
|
|
3002
3241
|
const bVal = accessor(b);
|
|
3003
|
-
if (aVal < bVal)
|
|
3004
|
-
|
|
3242
|
+
if (aVal < bVal) {
|
|
3243
|
+
return reversed ? 1 : -1;
|
|
3244
|
+
}
|
|
3245
|
+
if (aVal > bVal) {
|
|
3246
|
+
return reversed ? -1 : 1;
|
|
3247
|
+
}
|
|
3005
3248
|
return 0;
|
|
3006
3249
|
});
|
|
3007
3250
|
}
|
|
@@ -3138,8 +3381,12 @@ var init_query = __esm({
|
|
|
3138
3381
|
rows.sort((a, b) => {
|
|
3139
3382
|
const aVal = accessor(a);
|
|
3140
3383
|
const bVal = accessor(b);
|
|
3141
|
-
if (aVal < bVal)
|
|
3142
|
-
|
|
3384
|
+
if (aVal < bVal) {
|
|
3385
|
+
return this._reversed ? 1 : -1;
|
|
3386
|
+
}
|
|
3387
|
+
if (aVal > bVal) {
|
|
3388
|
+
return this._reversed ? -1 : 1;
|
|
3389
|
+
}
|
|
3143
3390
|
return 0;
|
|
3144
3391
|
});
|
|
3145
3392
|
}
|
|
@@ -3154,7 +3401,9 @@ var init_query = __esm({
|
|
|
3154
3401
|
if (this._predicates.length === 0) {
|
|
3155
3402
|
return { allSql: true, sqlWhere: "", compiled: [] };
|
|
3156
3403
|
}
|
|
3157
|
-
const compiled = this._predicates.map(
|
|
3404
|
+
const compiled = this._predicates.map(
|
|
3405
|
+
(p) => compilePredicate(p.fn, p.bindings)
|
|
3406
|
+
);
|
|
3158
3407
|
const allSql = compiled.every((c) => c.type === "sql");
|
|
3159
3408
|
let sqlWhere = "";
|
|
3160
3409
|
if (allSql) {
|
|
@@ -3308,7 +3557,9 @@ var init_table = __esm({
|
|
|
3308
3557
|
return this.filter(predicate, bindings).first();
|
|
3309
3558
|
}
|
|
3310
3559
|
count(predicate, bindings) {
|
|
3311
|
-
if (predicate)
|
|
3560
|
+
if (predicate) {
|
|
3561
|
+
return this.filter(predicate, bindings).count();
|
|
3562
|
+
}
|
|
3312
3563
|
return this.toArray().count();
|
|
3313
3564
|
}
|
|
3314
3565
|
some(predicate, bindings) {
|
|
@@ -3319,7 +3570,12 @@ var init_table = __esm({
|
|
|
3319
3570
|
}
|
|
3320
3571
|
/** True if the table has zero rows. */
|
|
3321
3572
|
async isEmpty() {
|
|
3322
|
-
const query = buildExists(
|
|
3573
|
+
const query = buildExists(
|
|
3574
|
+
this._config.tableName,
|
|
3575
|
+
void 0,
|
|
3576
|
+
void 0,
|
|
3577
|
+
true
|
|
3578
|
+
);
|
|
3323
3579
|
const results = await this._config.executeBatch([query]);
|
|
3324
3580
|
const row = results[0]?.rows[0];
|
|
3325
3581
|
return row?.result === 1;
|
|
@@ -3368,7 +3624,10 @@ var init_table = __esm({
|
|
|
3368
3624
|
return new Query(this._config);
|
|
3369
3625
|
}
|
|
3370
3626
|
filter(predicate, bindings) {
|
|
3371
|
-
return new Query(this._config).filter(
|
|
3627
|
+
return new Query(this._config).filter(
|
|
3628
|
+
predicate,
|
|
3629
|
+
bindings
|
|
3630
|
+
);
|
|
3372
3631
|
}
|
|
3373
3632
|
/** Sort rows by a field. Returns a chainable Query. */
|
|
3374
3633
|
sortBy(accessor) {
|
|
@@ -3436,25 +3695,29 @@ var init_table = __esm({
|
|
|
3436
3695
|
results[0].rows[0],
|
|
3437
3696
|
this._config.columns
|
|
3438
3697
|
);
|
|
3439
|
-
this._syncRolesIfNeeded(
|
|
3440
|
-
[data],
|
|
3441
|
-
result,
|
|
3442
|
-
false
|
|
3443
|
-
);
|
|
3698
|
+
this._syncRolesIfNeeded([data], result, false);
|
|
3444
3699
|
return result;
|
|
3445
3700
|
});
|
|
3446
3701
|
}
|
|
3447
3702
|
remove(id) {
|
|
3448
3703
|
const query = buildDelete(this._config.tableName, `id = ?`, [id]);
|
|
3449
|
-
return new Mutation(
|
|
3450
|
-
|
|
3451
|
-
|
|
3704
|
+
return new Mutation(
|
|
3705
|
+
this._config,
|
|
3706
|
+
[query],
|
|
3707
|
+
(results) => ({
|
|
3708
|
+
deleted: results[0].changes > 0
|
|
3709
|
+
})
|
|
3710
|
+
);
|
|
3452
3711
|
}
|
|
3453
3712
|
removeAll(predicate, bindings) {
|
|
3454
3713
|
const compiled = compilePredicate(predicate, bindings);
|
|
3455
3714
|
if (compiled.type === "sql") {
|
|
3456
3715
|
const query = buildDelete(this._config.tableName, compiled.where);
|
|
3457
|
-
return new Mutation(
|
|
3716
|
+
return new Mutation(
|
|
3717
|
+
this._config,
|
|
3718
|
+
[query],
|
|
3719
|
+
(results) => results[0].changes
|
|
3720
|
+
);
|
|
3458
3721
|
}
|
|
3459
3722
|
return Mutation.fromExecutor(this._config, async () => {
|
|
3460
3723
|
console.warn(
|
|
@@ -3469,8 +3732,12 @@ var init_table = __esm({
|
|
|
3469
3732
|
)
|
|
3470
3733
|
);
|
|
3471
3734
|
const matching = allRows.filter((row) => predicate(row, bindings));
|
|
3472
|
-
if (matching.length === 0)
|
|
3473
|
-
|
|
3735
|
+
if (matching.length === 0) {
|
|
3736
|
+
return 0;
|
|
3737
|
+
}
|
|
3738
|
+
const deleteQueries = matching.filter((row) => row.id).map(
|
|
3739
|
+
(row) => buildDelete(this._config.tableName, `id = ?`, [row.id])
|
|
3740
|
+
);
|
|
3474
3741
|
if (deleteQueries.length > 0) {
|
|
3475
3742
|
await this._config.executeBatch(deleteQueries);
|
|
3476
3743
|
}
|
|
@@ -3479,7 +3746,11 @@ var init_table = __esm({
|
|
|
3479
3746
|
}
|
|
3480
3747
|
clear() {
|
|
3481
3748
|
const query = buildDelete(this._config.tableName);
|
|
3482
|
-
return new Mutation(
|
|
3749
|
+
return new Mutation(
|
|
3750
|
+
this._config,
|
|
3751
|
+
[query],
|
|
3752
|
+
(results) => results[0].changes
|
|
3753
|
+
);
|
|
3483
3754
|
}
|
|
3484
3755
|
/**
|
|
3485
3756
|
* Insert a row, or update it if a row with the same unique key already
|
|
@@ -3534,7 +3805,9 @@ var init_table = __esm({
|
|
|
3534
3805
|
/** @internal Throw if data includes a platform-managed read-only column. */
|
|
3535
3806
|
_checkManagedColumns(data) {
|
|
3536
3807
|
const mc = this._config.managedColumns;
|
|
3537
|
-
if (!mc)
|
|
3808
|
+
if (!mc) {
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3538
3811
|
const keys = Object.keys(data);
|
|
3539
3812
|
for (const key of keys) {
|
|
3540
3813
|
if (mc.email && key === mc.email || mc.phone && key === mc.phone || mc.apiKey && key === mc.apiKey) {
|
|
@@ -3555,8 +3828,12 @@ var init_table = __esm({
|
|
|
3555
3828
|
_syncRolesIfNeeded(inputItems, result, isArray) {
|
|
3556
3829
|
const rolesCol = this._config.managedColumns?.roles;
|
|
3557
3830
|
const syncRoles = this._config.syncRoles;
|
|
3558
|
-
if (!rolesCol || !syncRoles)
|
|
3559
|
-
|
|
3831
|
+
if (!rolesCol || !syncRoles) {
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
if (!inputItems.some((item) => rolesCol in item)) {
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3560
3837
|
if (isArray) {
|
|
3561
3838
|
for (const row of result) {
|
|
3562
3839
|
if (row?.id) {
|
|
@@ -3715,7 +3992,9 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
3715
3992
|
for (let i = 0; i < compiled.length; i++) {
|
|
3716
3993
|
const c = compiled[i];
|
|
3717
3994
|
const dbId = c.config.databaseId;
|
|
3718
|
-
if (!groups.has(dbId))
|
|
3995
|
+
if (!groups.has(dbId)) {
|
|
3996
|
+
groups.set(dbId, []);
|
|
3997
|
+
}
|
|
3719
3998
|
if (c.type === "query") {
|
|
3720
3999
|
const sqlQuery = c.query ?? c.fallbackQuery;
|
|
3721
4000
|
groups.get(dbId).push({ opIndex: i, sqlQueries: [sqlQuery] });
|
|
@@ -3788,9 +4067,7 @@ function resolveDatabase(databases, hint) {
|
|
|
3788
4067
|
);
|
|
3789
4068
|
}
|
|
3790
4069
|
if (hint) {
|
|
3791
|
-
const targetDb = databases.find(
|
|
3792
|
-
(db) => db.id === hint || db.name === hint
|
|
3793
|
-
);
|
|
4070
|
+
const targetDb = databases.find((db) => db.id === hint || db.name === hint);
|
|
3794
4071
|
if (!targetDb) {
|
|
3795
4072
|
const available = databases.map((db) => db.name || db.id).join(", ");
|
|
3796
4073
|
throw new MindStudioError(
|
|
@@ -3947,7 +4224,10 @@ var init_store = __esm({
|
|
|
3947
4224
|
);
|
|
3948
4225
|
form.append(
|
|
3949
4226
|
"file",
|
|
3950
|
-
new Blob(
|
|
4227
|
+
new Blob(
|
|
4228
|
+
[view],
|
|
4229
|
+
options.contentType ? { type: options.contentType } : void 0
|
|
4230
|
+
)
|
|
3951
4231
|
);
|
|
3952
4232
|
const upload = await fetch(res.uploadUrl, {
|
|
3953
4233
|
method: "POST",
|
|
@@ -4644,9 +4924,13 @@ var init_events = __esm({
|
|
|
4644
4924
|
|
|
4645
4925
|
// src/task/schema.ts
|
|
4646
4926
|
function describe(value) {
|
|
4647
|
-
if (value === void 0)
|
|
4927
|
+
if (value === void 0) {
|
|
4928
|
+
return "undefined";
|
|
4929
|
+
}
|
|
4648
4930
|
const json = JSON.stringify(value);
|
|
4649
|
-
if (json === void 0)
|
|
4931
|
+
if (json === void 0) {
|
|
4932
|
+
return String(value);
|
|
4933
|
+
}
|
|
4650
4934
|
return json.length > 60 ? `${json.slice(0, 57)}...` : json;
|
|
4651
4935
|
}
|
|
4652
4936
|
function matchesTypeName(value, name) {
|
|
@@ -4745,7 +5029,9 @@ function assertSupportedSchema(schema) {
|
|
|
4745
5029
|
}
|
|
4746
5030
|
function assertSupportedNode(schema, path) {
|
|
4747
5031
|
for (const key of Object.keys(schema)) {
|
|
4748
|
-
if (SUPPORTED_KEYWORDS.has(key))
|
|
5032
|
+
if (SUPPORTED_KEYWORDS.has(key)) {
|
|
5033
|
+
continue;
|
|
5034
|
+
}
|
|
4749
5035
|
const hint = key === "nullable" ? ' Use a type array instead: type: ["string", "null"].' : key === "oneOf" || key === "anyOf" || key === "allOf" ? " Model alternatives with an enum, a type array, or a discriminating property instead." : "";
|
|
4750
5036
|
throw new MindStudioError(
|
|
4751
5037
|
`[task] Unsupported JSON Schema keyword "${key}" at ${path} in outputSchema. Supported: type, properties, required, additionalProperties, items, enum, const.${hint}`,
|
|
@@ -4763,17 +5049,18 @@ function assertSupportedNode(schema, path) {
|
|
|
4763
5049
|
}
|
|
4764
5050
|
}
|
|
4765
5051
|
function buildExampleFromSchema(schema) {
|
|
4766
|
-
if (schema.const !== void 0)
|
|
4767
|
-
|
|
5052
|
+
if (schema.const !== void 0) {
|
|
5053
|
+
return schema.const;
|
|
5054
|
+
}
|
|
5055
|
+
if (schema.enum !== void 0 && schema.enum.length > 0) {
|
|
4768
5056
|
return schema.enum[0];
|
|
5057
|
+
}
|
|
4769
5058
|
const names = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
4770
5059
|
const name = names.find((n) => n !== "null") ?? names[0];
|
|
4771
5060
|
switch (name) {
|
|
4772
5061
|
case "object": {
|
|
4773
5062
|
const example = {};
|
|
4774
|
-
for (const [key, propSchema] of Object.entries(
|
|
4775
|
-
schema.properties ?? {}
|
|
4776
|
-
)) {
|
|
5063
|
+
for (const [key, propSchema] of Object.entries(schema.properties ?? {})) {
|
|
4777
5064
|
example[key] = buildExampleFromSchema(propSchema);
|
|
4778
5065
|
}
|
|
4779
5066
|
return example;
|
|
@@ -4872,7 +5159,9 @@ function isDevMode() {
|
|
|
4872
5159
|
return !!(process.env.CALLBACK_TOKEN || getRequestContext()?.callbackToken);
|
|
4873
5160
|
}
|
|
4874
5161
|
function logTaskResult(result) {
|
|
4875
|
-
if (!isDevMode())
|
|
5162
|
+
if (!isDevMode()) {
|
|
5163
|
+
return;
|
|
5164
|
+
}
|
|
4876
5165
|
const toolSummary = result.toolCalls.map((tc) => `${tc.name} (${tc.durationMs}ms) ${tc.success ? "\u2713" : "\u2717"}`).join(", ");
|
|
4877
5166
|
console.log(
|
|
4878
5167
|
`[task] ${result.turns} turn${result.turns === 1 ? "" : "s"}` + (toolSummary ? `: ${toolSummary}` : "") + ` | ${result.parsedSuccessfully ? "output OK" : "\u26A0 output not valid JSON"} | cost: ${result.usage.totalBillingCost}`
|
|
@@ -4894,8 +5183,9 @@ async function runTaskPoll(httpConfig, body) {
|
|
|
4894
5183
|
const res = await fetch(pollUrl, {
|
|
4895
5184
|
headers: { "User-Agent": "@mindstudio-ai/agent" }
|
|
4896
5185
|
});
|
|
4897
|
-
if (res.status === 502 || res.status === 503 || res.status === 504)
|
|
5186
|
+
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
4898
5187
|
continue;
|
|
5188
|
+
}
|
|
4899
5189
|
if (res.status === 404) {
|
|
4900
5190
|
throw new MindStudioError(
|
|
4901
5191
|
"Task poll token not found or expired.",
|
|
@@ -4968,11 +5258,17 @@ async function runTaskStream(httpConfig, body, onEvent) {
|
|
|
4968
5258
|
const errBody = JSON.parse(text);
|
|
4969
5259
|
details = errBody;
|
|
4970
5260
|
const errMsg = (typeof errBody.error === "string" ? errBody.error : void 0) ?? (typeof errBody.message === "string" ? errBody.message : void 0);
|
|
4971
|
-
if (errMsg)
|
|
4972
|
-
|
|
5261
|
+
if (errMsg) {
|
|
5262
|
+
message = errMsg;
|
|
5263
|
+
}
|
|
5264
|
+
if (errBody.code) {
|
|
5265
|
+
code = errBody.code;
|
|
5266
|
+
}
|
|
4973
5267
|
} catch {
|
|
4974
5268
|
const stripped = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
4975
|
-
if (stripped)
|
|
5269
|
+
if (stripped) {
|
|
5270
|
+
message = stripped.slice(0, 200);
|
|
5271
|
+
}
|
|
4976
5272
|
}
|
|
4977
5273
|
} catch {
|
|
4978
5274
|
}
|
|
@@ -4984,12 +5280,16 @@ async function runTaskStream(httpConfig, body, onEvent) {
|
|
|
4984
5280
|
let result = null;
|
|
4985
5281
|
while (true) {
|
|
4986
5282
|
const { done, value } = await reader.read();
|
|
4987
|
-
if (done)
|
|
5283
|
+
if (done) {
|
|
5284
|
+
break;
|
|
5285
|
+
}
|
|
4988
5286
|
buffer += decoder.decode(value, { stream: true });
|
|
4989
5287
|
const lines = buffer.split("\n");
|
|
4990
5288
|
buffer = lines.pop() ?? "";
|
|
4991
5289
|
for (const line of lines) {
|
|
4992
|
-
if (!line.startsWith("data: "))
|
|
5290
|
+
if (!line.startsWith("data: ")) {
|
|
5291
|
+
continue;
|
|
5292
|
+
}
|
|
4993
5293
|
try {
|
|
4994
5294
|
const event = JSON.parse(line.slice(6));
|
|
4995
5295
|
onEvent(event);
|
|
@@ -5016,7 +5316,9 @@ async function runTaskStream(httpConfig, body, onEvent) {
|
|
|
5016
5316
|
};
|
|
5017
5317
|
}
|
|
5018
5318
|
} catch (err) {
|
|
5019
|
-
if (err instanceof MindStudioError)
|
|
5319
|
+
if (err instanceof MindStudioError) {
|
|
5320
|
+
throw err;
|
|
5321
|
+
}
|
|
5020
5322
|
}
|
|
5021
5323
|
}
|
|
5022
5324
|
}
|
|
@@ -5047,7 +5349,9 @@ async function runTaskStream(httpConfig, body, onEvent) {
|
|
|
5047
5349
|
};
|
|
5048
5350
|
}
|
|
5049
5351
|
} catch (err) {
|
|
5050
|
-
if (err instanceof MindStudioError)
|
|
5352
|
+
if (err instanceof MindStudioError) {
|
|
5353
|
+
throw err;
|
|
5354
|
+
}
|
|
5051
5355
|
}
|
|
5052
5356
|
}
|
|
5053
5357
|
if (!result) {
|
|
@@ -5125,8 +5429,12 @@ async function attemptTurn(httpConfig, body) {
|
|
|
5125
5429
|
try {
|
|
5126
5430
|
const errBody = await res.json();
|
|
5127
5431
|
const errMsg = (typeof errBody.errorMessage === "string" ? errBody.errorMessage : void 0) ?? (typeof errBody.errorString === "string" ? errBody.errorString : void 0) ?? (typeof errBody.error === "string" ? errBody.error : void 0);
|
|
5128
|
-
if (errMsg)
|
|
5129
|
-
|
|
5432
|
+
if (errMsg) {
|
|
5433
|
+
message = errMsg;
|
|
5434
|
+
}
|
|
5435
|
+
if (typeof errBody.errorString === "string") {
|
|
5436
|
+
code = errBody.errorString;
|
|
5437
|
+
}
|
|
5130
5438
|
} catch {
|
|
5131
5439
|
}
|
|
5132
5440
|
const retryable = res.status >= 500 || res.status === 429;
|
|
@@ -5254,7 +5562,9 @@ async function runTaskLocal(deps, options) {
|
|
|
5254
5562
|
const { httpConfig } = deps;
|
|
5255
5563
|
const onEvent = options.onEvent;
|
|
5256
5564
|
const outputSchema = options.outputSchema;
|
|
5257
|
-
if (outputSchema)
|
|
5565
|
+
if (outputSchema) {
|
|
5566
|
+
assertSupportedSchema(outputSchema);
|
|
5567
|
+
}
|
|
5258
5568
|
let system;
|
|
5259
5569
|
if (outputSchema) {
|
|
5260
5570
|
system = `${options.prompt}
|
|
@@ -5282,12 +5592,16 @@ ${structuredOutputExample}
|
|
|
5282
5592
|
for (const t of wireTools) {
|
|
5283
5593
|
if ("appMethod" in t) {
|
|
5284
5594
|
toolKinds.set(t.appMethod, "method");
|
|
5285
|
-
if (t.defaults)
|
|
5595
|
+
if (t.defaults) {
|
|
5596
|
+
toolDefaults.set(t.appMethod, t.defaults);
|
|
5597
|
+
}
|
|
5286
5598
|
} else if ("name" in t) {
|
|
5287
5599
|
toolKinds.set(t.name, "function");
|
|
5288
5600
|
} else {
|
|
5289
5601
|
toolKinds.set(t.stepType, "step");
|
|
5290
|
-
if (t.defaults)
|
|
5602
|
+
if (t.defaults) {
|
|
5603
|
+
toolDefaults.set(t.stepType, t.defaults);
|
|
5604
|
+
}
|
|
5291
5605
|
}
|
|
5292
5606
|
}
|
|
5293
5607
|
const maxTurns = Math.min(
|
|
@@ -5420,7 +5734,11 @@ ${structuredOutputExample}
|
|
|
5420
5734
|
const output3 = await functionTools.get(toolCall.name)(
|
|
5421
5735
|
mergedInput
|
|
5422
5736
|
);
|
|
5423
|
-
result = {
|
|
5737
|
+
result = {
|
|
5738
|
+
output: output3 ?? null,
|
|
5739
|
+
billingCost: 0,
|
|
5740
|
+
isError: false
|
|
5741
|
+
};
|
|
5424
5742
|
} catch (err) {
|
|
5425
5743
|
result = {
|
|
5426
5744
|
output: {
|
|
@@ -5468,7 +5786,9 @@ ${structuredOutputExample}
|
|
|
5468
5786
|
let output2;
|
|
5469
5787
|
let parseOk = true;
|
|
5470
5788
|
try {
|
|
5471
|
-
output2 = JSON.parse(
|
|
5789
|
+
output2 = JSON.parse(
|
|
5790
|
+
outputSchema ? stripCodeFences(turn.text) : turn.text
|
|
5791
|
+
);
|
|
5472
5792
|
} catch {
|
|
5473
5793
|
parseOk = false;
|
|
5474
5794
|
}
|
|
@@ -6064,14 +6384,21 @@ function resolveStepType2(name) {
|
|
|
6064
6384
|
}
|
|
6065
6385
|
function resolveToken(provided, config) {
|
|
6066
6386
|
const rctx = getRequestContext();
|
|
6067
|
-
if (rctx?.callbackToken)
|
|
6387
|
+
if (rctx?.callbackToken) {
|
|
6068
6388
|
return { token: rctx.callbackToken, authType: "internal" };
|
|
6069
|
-
|
|
6389
|
+
}
|
|
6390
|
+
if (process.env.CALLBACK_TOKEN) {
|
|
6070
6391
|
return { token: process.env.CALLBACK_TOKEN, authType: "internal" };
|
|
6071
|
-
|
|
6072
|
-
if (
|
|
6392
|
+
}
|
|
6393
|
+
if (provided) {
|
|
6394
|
+
return { token: provided, authType: "apiKey" };
|
|
6395
|
+
}
|
|
6396
|
+
if (process.env.MINDSTUDIO_API_KEY) {
|
|
6073
6397
|
return { token: process.env.MINDSTUDIO_API_KEY, authType: "apiKey" };
|
|
6074
|
-
|
|
6398
|
+
}
|
|
6399
|
+
if (config?.apiKey) {
|
|
6400
|
+
return { token: config.apiKey, authType: "apiKey" };
|
|
6401
|
+
}
|
|
6075
6402
|
throw new MindStudioError(
|
|
6076
6403
|
"No API key provided. Run `mindstudio login`, pass `apiKey` to the constructor, or set the MINDSTUDIO_API_KEY environment variable.",
|
|
6077
6404
|
"missing_api_key",
|
|
@@ -6153,7 +6480,9 @@ var init_client = __esm({
|
|
|
6153
6480
|
*/
|
|
6154
6481
|
get _token() {
|
|
6155
6482
|
const rctx = getRequestContext();
|
|
6156
|
-
if (rctx?.callbackToken)
|
|
6483
|
+
if (rctx?.callbackToken) {
|
|
6484
|
+
return rctx.callbackToken;
|
|
6485
|
+
}
|
|
6157
6486
|
if (this._authType === "internal" && process.env.CALLBACK_TOKEN) {
|
|
6158
6487
|
return process.env.CALLBACK_TOKEN;
|
|
6159
6488
|
}
|
|
@@ -6258,8 +6587,9 @@ var init_client = __esm({
|
|
|
6258
6587
|
const res = await fetch(pollUrl, {
|
|
6259
6588
|
headers: { "User-Agent": "@mindstudio-ai/agent" }
|
|
6260
6589
|
});
|
|
6261
|
-
if (res.status === 502 || res.status === 503 || res.status === 504)
|
|
6590
|
+
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
6262
6591
|
continue;
|
|
6592
|
+
}
|
|
6263
6593
|
if (res.status === 404) {
|
|
6264
6594
|
throw new MindStudioError(
|
|
6265
6595
|
`[${stepType}] Execution token expired.`,
|
|
@@ -6277,7 +6607,9 @@ var init_client = __esm({
|
|
|
6277
6607
|
);
|
|
6278
6608
|
}
|
|
6279
6609
|
const poll = await res.json();
|
|
6280
|
-
if (poll.status === "pending")
|
|
6610
|
+
if (poll.status === "pending") {
|
|
6611
|
+
continue;
|
|
6612
|
+
}
|
|
6281
6613
|
if (poll.status === "error") {
|
|
6282
6614
|
throw new MindStudioError(
|
|
6283
6615
|
`[${stepType}] ${poll.error ?? "Step execution failed."}`,
|
|
@@ -6358,16 +6690,21 @@ var init_client = __esm({
|
|
|
6358
6690
|
const body2 = JSON.parse(text);
|
|
6359
6691
|
details = body2;
|
|
6360
6692
|
const errMsg = (typeof body2.error === "string" ? body2.error : void 0) ?? (typeof body2.message === "string" ? body2.message : void 0) ?? (typeof body2.details === "string" ? body2.details : void 0);
|
|
6361
|
-
if (errMsg)
|
|
6362
|
-
|
|
6693
|
+
if (errMsg) {
|
|
6694
|
+
message = errMsg;
|
|
6695
|
+
} else if (body2.error || body2.message || body2.details) {
|
|
6363
6696
|
message = JSON.stringify(
|
|
6364
6697
|
body2.error ?? body2.message ?? body2.details
|
|
6365
6698
|
);
|
|
6366
6699
|
}
|
|
6367
|
-
if (body2.code)
|
|
6700
|
+
if (body2.code) {
|
|
6701
|
+
code = body2.code;
|
|
6702
|
+
}
|
|
6368
6703
|
} catch {
|
|
6369
6704
|
const stripped = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
6370
|
-
if (stripped)
|
|
6705
|
+
if (stripped) {
|
|
6706
|
+
message = stripped.slice(0, 200);
|
|
6707
|
+
}
|
|
6371
6708
|
}
|
|
6372
6709
|
} catch {
|
|
6373
6710
|
}
|
|
@@ -6386,12 +6723,16 @@ var init_client = __esm({
|
|
|
6386
6723
|
let doneEvent = null;
|
|
6387
6724
|
while (true) {
|
|
6388
6725
|
const { done, value } = await reader.read();
|
|
6389
|
-
if (done)
|
|
6726
|
+
if (done) {
|
|
6727
|
+
break;
|
|
6728
|
+
}
|
|
6390
6729
|
buffer += decoder.decode(value, { stream: true });
|
|
6391
6730
|
const lines = buffer.split("\n");
|
|
6392
6731
|
buffer = lines.pop() ?? "";
|
|
6393
6732
|
for (const line of lines) {
|
|
6394
|
-
if (!line.startsWith("data: "))
|
|
6733
|
+
if (!line.startsWith("data: ")) {
|
|
6734
|
+
continue;
|
|
6735
|
+
}
|
|
6395
6736
|
try {
|
|
6396
6737
|
const event = JSON.parse(line.slice(6));
|
|
6397
6738
|
if (event.type === "log") {
|
|
@@ -6415,7 +6756,9 @@ var init_client = __esm({
|
|
|
6415
6756
|
);
|
|
6416
6757
|
}
|
|
6417
6758
|
} catch (err) {
|
|
6418
|
-
if (err instanceof MindStudioError)
|
|
6759
|
+
if (err instanceof MindStudioError) {
|
|
6760
|
+
throw err;
|
|
6761
|
+
}
|
|
6419
6762
|
}
|
|
6420
6763
|
}
|
|
6421
6764
|
}
|
|
@@ -6443,7 +6786,9 @@ var init_client = __esm({
|
|
|
6443
6786
|
});
|
|
6444
6787
|
}
|
|
6445
6788
|
} catch (err) {
|
|
6446
|
-
if (err instanceof MindStudioError)
|
|
6789
|
+
if (err instanceof MindStudioError) {
|
|
6790
|
+
throw err;
|
|
6791
|
+
}
|
|
6447
6792
|
}
|
|
6448
6793
|
}
|
|
6449
6794
|
if (!doneEvent) {
|
|
@@ -6527,8 +6872,9 @@ var init_client = __esm({
|
|
|
6527
6872
|
const res = await fetch(pollUrl, {
|
|
6528
6873
|
headers: { "User-Agent": "@mindstudio-ai/agent" }
|
|
6529
6874
|
});
|
|
6530
|
-
if (res.status === 502 || res.status === 503 || res.status === 504)
|
|
6875
|
+
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
6531
6876
|
continue;
|
|
6877
|
+
}
|
|
6532
6878
|
if (res.status === 404) {
|
|
6533
6879
|
throw new MindStudioError(
|
|
6534
6880
|
"Batch poll token not found or expired.",
|
|
@@ -6822,8 +7168,9 @@ var init_client = __esm({
|
|
|
6822
7168
|
const res = await fetch(pollUrl, {
|
|
6823
7169
|
headers: { "User-Agent": "@mindstudio-ai/agent" }
|
|
6824
7170
|
});
|
|
6825
|
-
if (res.status === 502 || res.status === 503 || res.status === 504)
|
|
7171
|
+
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
6826
7172
|
continue;
|
|
7173
|
+
}
|
|
6827
7174
|
if (res.status === 404) {
|
|
6828
7175
|
throw new MindStudioError(
|
|
6829
7176
|
"Poll token not found or expired.",
|
|
@@ -6841,7 +7188,9 @@ var init_client = __esm({
|
|
|
6841
7188
|
);
|
|
6842
7189
|
}
|
|
6843
7190
|
const poll = await res.json();
|
|
6844
|
-
if (poll.status === "pending")
|
|
7191
|
+
if (poll.status === "pending") {
|
|
7192
|
+
continue;
|
|
7193
|
+
}
|
|
6845
7194
|
if (poll.status === "error") {
|
|
6846
7195
|
throw new MindStudioError(
|
|
6847
7196
|
poll.error ?? "Agent run failed.",
|
|
@@ -6984,7 +7333,9 @@ var init_client = __esm({
|
|
|
6984
7333
|
*/
|
|
6985
7334
|
stream = async (data) => {
|
|
6986
7335
|
const streamId = this._currentStreamId;
|
|
6987
|
-
if (!streamId)
|
|
7336
|
+
if (!streamId) {
|
|
7337
|
+
return;
|
|
7338
|
+
}
|
|
6988
7339
|
const url = `${this._currentHttpConfig.baseUrl}/_internal/v2/stream-chunk`;
|
|
6989
7340
|
const body = typeof data === "string" ? { streamId, type: "token", text: data } : { streamId, type: "data", data };
|
|
6990
7341
|
const res = await fetch(url, {
|
|
@@ -7068,7 +7419,9 @@ var init_client = __esm({
|
|
|
7068
7419
|
if (!this._db) {
|
|
7069
7420
|
this._trySandboxHydration();
|
|
7070
7421
|
}
|
|
7071
|
-
if (this._db)
|
|
7422
|
+
if (this._db) {
|
|
7423
|
+
return this._db;
|
|
7424
|
+
}
|
|
7072
7425
|
return this._createLazyDb();
|
|
7073
7426
|
}
|
|
7074
7427
|
/**
|
|
@@ -7093,8 +7446,12 @@ var init_client = __esm({
|
|
|
7093
7446
|
* ```
|
|
7094
7447
|
*/
|
|
7095
7448
|
async ensureContext() {
|
|
7096
|
-
if (this._getContext())
|
|
7097
|
-
|
|
7449
|
+
if (this._getContext()) {
|
|
7450
|
+
return;
|
|
7451
|
+
}
|
|
7452
|
+
if (this._context) {
|
|
7453
|
+
return;
|
|
7454
|
+
}
|
|
7098
7455
|
if (!this._contextPromise) {
|
|
7099
7456
|
this._contextPromise = this._hydrateContext();
|
|
7100
7457
|
}
|
|
@@ -7140,7 +7497,9 @@ var init_client = __esm({
|
|
|
7140
7497
|
* - `ai.databases`: [{ id, name, tables[] }]
|
|
7141
7498
|
*/
|
|
7142
7499
|
_trySandboxHydration() {
|
|
7143
|
-
if (getRequestContext())
|
|
7500
|
+
if (getRequestContext()) {
|
|
7501
|
+
return;
|
|
7502
|
+
}
|
|
7144
7503
|
const ai = globalThis.ai;
|
|
7145
7504
|
if (ai?.auth && ai?.databases) {
|
|
7146
7505
|
this._applyContext({
|
|
@@ -7300,7 +7659,9 @@ var init_client = __esm({
|
|
|
7300
7659
|
try {
|
|
7301
7660
|
const errBody = await res.json();
|
|
7302
7661
|
details = errBody;
|
|
7303
|
-
if (typeof errBody.errorString === "string")
|
|
7662
|
+
if (typeof errBody.errorString === "string") {
|
|
7663
|
+
code = errBody.errorString;
|
|
7664
|
+
}
|
|
7304
7665
|
message = typeof errBody.errorMessage === "string" && errBody.errorMessage || typeof errBody.errorString === "string" && errBody.errorString || message;
|
|
7305
7666
|
} catch {
|
|
7306
7667
|
}
|
|
@@ -7502,15 +7863,20 @@ var init_client = __esm({
|
|
|
7502
7863
|
try {
|
|
7503
7864
|
const body = JSON.parse(text);
|
|
7504
7865
|
const errMsg = (typeof body.error === "string" ? body.error : void 0) ?? (typeof body.message === "string" ? body.message : void 0) ?? (typeof body.details === "string" ? body.details : void 0);
|
|
7505
|
-
if (errMsg)
|
|
7506
|
-
|
|
7866
|
+
if (errMsg) {
|
|
7867
|
+
message = errMsg;
|
|
7868
|
+
} else if (body.error || body.message || body.details) {
|
|
7507
7869
|
message = JSON.stringify(
|
|
7508
7870
|
body.error ?? body.message ?? body.details
|
|
7509
7871
|
);
|
|
7510
7872
|
}
|
|
7511
|
-
if (body.code)
|
|
7873
|
+
if (body.code) {
|
|
7874
|
+
code = body.code;
|
|
7875
|
+
}
|
|
7512
7876
|
} catch {
|
|
7513
|
-
if (text && text.length < 500)
|
|
7877
|
+
if (text && text.length < 500) {
|
|
7878
|
+
message = text;
|
|
7879
|
+
}
|
|
7514
7880
|
}
|
|
7515
7881
|
} catch {
|
|
7516
7882
|
}
|
|
@@ -7773,7 +8139,9 @@ var init_client = __esm({
|
|
|
7773
8139
|
try {
|
|
7774
8140
|
const body = await res.json();
|
|
7775
8141
|
details = body;
|
|
7776
|
-
if (typeof body.errorString === "string")
|
|
8142
|
+
if (typeof body.errorString === "string") {
|
|
8143
|
+
code = body.errorString;
|
|
8144
|
+
}
|
|
7777
8145
|
message = typeof body.errorMessage === "string" && body.errorMessage || typeof body.errorString === "string" && body.errorString || message;
|
|
7778
8146
|
} catch {
|
|
7779
8147
|
}
|
|
@@ -7817,7 +8185,9 @@ var init_client = __esm({
|
|
|
7817
8185
|
try {
|
|
7818
8186
|
const body = await res.json();
|
|
7819
8187
|
details = body;
|
|
7820
|
-
if (typeof body.errorString === "string")
|
|
8188
|
+
if (typeof body.errorString === "string") {
|
|
8189
|
+
code = body.errorString;
|
|
8190
|
+
}
|
|
7821
8191
|
message = typeof body.errorMessage === "string" && body.errorMessage || typeof body.errorString === "string" && body.errorString || message;
|
|
7822
8192
|
} catch {
|
|
7823
8193
|
}
|
|
@@ -7888,7 +8258,9 @@ var init_client = __esm({
|
|
|
7888
8258
|
const filename = options.filename ?? `upload.${options.extension}`;
|
|
7889
8259
|
const { data } = await request(this._currentHttpConfig, "POST", "/account/upload", { filename });
|
|
7890
8260
|
const form = new FormData();
|
|
7891
|
-
for (const [k, v] of Object.entries(data.fields))
|
|
8261
|
+
for (const [k, v] of Object.entries(data.fields)) {
|
|
8262
|
+
form.append(k, v);
|
|
8263
|
+
}
|
|
7892
8264
|
const buf = content.buffer.slice(
|
|
7893
8265
|
content.byteOffset,
|
|
7894
8266
|
content.byteOffset + content.byteLength
|
|
@@ -7944,8 +8316,12 @@ async function* streamChat(params) {
|
|
|
7944
8316
|
let errorMessage = `HTTP ${res.status}`;
|
|
7945
8317
|
try {
|
|
7946
8318
|
const body2 = await res.json();
|
|
7947
|
-
if (body2.error)
|
|
7948
|
-
|
|
8319
|
+
if (body2.error) {
|
|
8320
|
+
errorMessage = body2.error;
|
|
8321
|
+
}
|
|
8322
|
+
if (body2.errorMessage) {
|
|
8323
|
+
errorMessage = body2.errorMessage;
|
|
8324
|
+
}
|
|
7949
8325
|
} catch {
|
|
7950
8326
|
}
|
|
7951
8327
|
yield { type: "error", error: errorMessage };
|
|
@@ -7956,12 +8332,16 @@ async function* streamChat(params) {
|
|
|
7956
8332
|
let buffer = "";
|
|
7957
8333
|
while (true) {
|
|
7958
8334
|
const { done, value } = await reader.read();
|
|
7959
|
-
if (done)
|
|
8335
|
+
if (done) {
|
|
8336
|
+
break;
|
|
8337
|
+
}
|
|
7960
8338
|
buffer += decoder.decode(value, { stream: true });
|
|
7961
8339
|
const lines = buffer.split("\n");
|
|
7962
8340
|
buffer = lines.pop() ?? "";
|
|
7963
8341
|
for (const line of lines) {
|
|
7964
|
-
if (!line.startsWith("data: "))
|
|
8342
|
+
if (!line.startsWith("data: ")) {
|
|
8343
|
+
continue;
|
|
8344
|
+
}
|
|
7965
8345
|
try {
|
|
7966
8346
|
const event = JSON.parse(line.slice(6));
|
|
7967
8347
|
if (event.type === "text" || event.type === "tool_use" || event.type === "done" || event.type === "error") {
|
|
@@ -8543,7 +8923,7 @@ var llmsContent;
|
|
|
8543
8923
|
var init_llms_content = __esm({
|
|
8544
8924
|
"src/generated/llms-content.ts"() {
|
|
8545
8925
|
"use strict";
|
|
8546
|
-
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", "call_cap_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#### analyzeImage\nAnalyze an image using a vision model based on a text prompt.\n- Uses the configured vision model to generate a text analysis of the image.\n- The prompt should describe what to look for or extract from the image.\n- Pass imageUrl for a single image, or imageUrls for multiple images analyzed together in one request.\n- Most vision models (OpenAI, Grok, Gemini) accept multiple images in one request. Ideogram describe is single-image only.\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, config?: object } }`\n- Output: `{ analysis: string }`\n\n#### analyzeVideo\nAnalyze a video using a video analysis model based on a text prompt.\n- Uses the configured video analysis model to generate a text analysis of the video.\n- The prompt should describe what to look for or extract from the video.\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, 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: `unknown`\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#### 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: `unknown`\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: `unknown`\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, 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#### 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- 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- Optionally set `model` to a specific document-extraction model (`mistral-ocr-latest`, `llamaparse`, `google-document-ai`); omit to use the platform default.\n- Maximum file size is 50MB per URL.\n- Input: `{ url: string | string[], model?: string }`\n- Output: `{ text: string | string[] }`\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: `unknown`\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- Text-to-3D models use the prompt field.\n- Image-to-3D and multi-view models take image URLs through the selected model\'s configuration inputs.\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, 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 using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences).\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, multiple images 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, 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 provided audio and image.\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: `unknown`\n\n#### generateMusic\nGenerate an audio file from provided instructions (text) using a music model.\n- The text field contains the instructions (prompt) for the music generation.\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: `unknown`\n\n#### generateSound\nGenerate a sound effect from a text description using a sound model.\n- The text field contains the description of the sound effect to generate.\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 V2ExecutionService.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, 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 using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences).\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: `unknown`\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: `unknown`\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#### 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: `unknown`\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: `unknown`\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: `unknown`\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, 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#### resizeVideo\nResize a video file\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#### 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, 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: `unknown`\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: `unknown`\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- 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 prompt field provides optional context to improve transcription accuracy (e.g. language, speaker names, domain).\n- Detail beyond the plain transcript is requested through `transcriptionModelOverride.config`, and support varies by model:\n- `includeSpeakers: \'yes\'` \u2014 label each passage with a speaker. Supported by elevenlabs-scribe-v1, elevenlabs-scribe-v2, deepgram-nova-3 and grok-stt. Also switches `text` to a timestamped, speaker-labelled block format.\n- `includeWordTimings: \'yes\'` \u2014 add per-word timings and confidence inside each segment. Supported by elevenlabs-scribe-v1, elevenlabs-scribe-v2, deepgram-nova-3 and whisper-1. Off by default because an hour of speech is roughly ten thousand words.\n- `detectLanguage: \'yes\'` \u2014 detect the spoken language instead of assuming English (deepgram-nova-3 only; Scribe and the whisper models always report it).\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 for elevenlabs-scribe-v1/v2, deepgram-nova-3, whisper-1 and the DeepInfra-served whisper/voxtral/qwen models, and absent for models that return nothing but text (gpt-transcribe, gpt-4o-transcribe, grok-stt).\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 }`\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: `unknown`\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\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, 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### 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 per their `Cache-Control`, set at put time via `cacheControl`: auto-minted keys default to immutable (cache-forever \u2014 the key is never reused), and named keys default to `public, max-age=300`, so overwriting one propagates publicly 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` \u2014 any size up to 5 GiB (large content transparently uploads direct to storage via a presigned POST, so there is no request-body cap). Omit `key` for a random UUID; `contentAddressed: true` derives an immutable `<sha256>.<ext>` key. `cacheControl` sets the CDN caching of a public object (defaults: auto-minted keys immutable, named keys `public, max-age=300`).\n- `get(key)` \u2192 `Buffer` \xB7 `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```\n\nGenerated assets: any action that produces a file (`generateImage`, `generateVideo`, `generateSpeech`, `generatePdf`, `upscaleImage`, \u2026) can put its output straight into one of these stores \u2014 pass the store handle as `store` in the OPTIONS object (second argument). 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.\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\n`SearchHit`: `{ score, text, 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`, `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 updates the tags in place \u2014 no re-processing.\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 \u2014 how an app builds its own timeline or index of a big source after ingest, in a background task), `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 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, bytes(), text(), json() }` and returns one of `documents([{ externalId, title, markdown, metadata?, replaces? }])`, `passthrough({ metadata? })` (ingest the raw object as-is), `skip(reason)`, or `deletes([externalId, ...])`; throwing is the object\'s error. Everything entering a mapped source is mapped \u2014 jobs, syncs and `add()` alike \u2014 so `add()` on a mapped source returns `documents` (possibly several) and throws `mapper_skipped` for an object the mapper refused. 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\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';
|
|
8926
|
+
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", "call_cap_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#### analyzeImage\nAnalyze an image using a vision model based on a text prompt.\n- Uses the configured vision model to generate a text analysis of the image.\n- The prompt should describe what to look for or extract from the image.\n- Pass imageUrl for a single image, or imageUrls for multiple images analyzed together in one request.\n- Most vision models (OpenAI, Grok, Gemini) accept multiple images in one request. Ideogram describe is single-image only.\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, config?: object } }`\n- Output: `{ analysis: string }`\n\n#### analyzeVideo\nAnalyze a video using a video analysis model based on a text prompt.\n- Uses the configured video analysis model to generate a text analysis of the video.\n- The prompt should describe what to look for or extract from the video.\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, 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: `unknown`\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#### 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: `unknown`\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: `unknown`\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, 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#### 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- 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- Optionally set `model` to a specific document-extraction model (`mistral-ocr-latest`, `llamaparse`, `google-document-ai`); omit to use the platform default.\n- Maximum file size is 50MB per URL.\n- Input: `{ url: string | string[], model?: string }`\n- Output: `{ text: string | string[] }`\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: `unknown`\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- Text-to-3D models use the prompt field.\n- Image-to-3D and multi-view models take image URLs through the selected model\'s configuration inputs.\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, 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 using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences).\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, multiple images 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, 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 provided audio and image.\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: `unknown`\n\n#### generateMusic\nGenerate an audio file from provided instructions (text) using a music model.\n- The text field contains the instructions (prompt) for the music generation.\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: `unknown`\n\n#### generateSound\nGenerate a sound effect from a text description using a sound model.\n- The text field contains the description of the sound effect to generate.\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, 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 using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences).\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: `unknown`\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: `unknown`\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#### 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: `unknown`\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: `unknown`\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: `unknown`\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, 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#### resizeVideo\nResize a video file\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#### 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, 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: `unknown`\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: `unknown`\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- 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 prompt field provides optional context to improve transcription accuracy (e.g. language, speaker names, domain).\n- Detail beyond the plain transcript is requested through `transcriptionModelOverride.config`, and support varies by model:\n- `includeSpeakers: \'yes\'` \u2014 label each passage with a speaker. Supported by elevenlabs-scribe-v1, elevenlabs-scribe-v2, deepgram-nova-3 and grok-stt. Also switches `text` to a timestamped, speaker-labelled block format.\n- `includeWordTimings: \'yes\'` \u2014 add per-word timings and confidence inside each segment. Supported by elevenlabs-scribe-v1, elevenlabs-scribe-v2, deepgram-nova-3 and whisper-1. Off by default because an hour of speech is roughly ten thousand words.\n- `detectLanguage: \'yes\'` \u2014 detect the spoken language instead of assuming English (deepgram-nova-3 only; Scribe and the whisper models always report it).\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 for elevenlabs-scribe-v1/v2, deepgram-nova-3, whisper-1 and the DeepInfra-served whisper/voxtral/qwen models, and absent for models that return nothing but text (gpt-transcribe, gpt-4o-transcribe, grok-stt).\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 }`\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: `unknown`\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### 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 per their `Cache-Control`, set at put time via `cacheControl`: auto-minted keys default to immutable (cache-forever \u2014 the key is never reused), and named keys default to `public, max-age=300`, so overwriting one propagates publicly 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` \u2014 any size up to 5 GiB (large content transparently uploads direct to storage via a presigned POST, so there is no request-body cap). Omit `key` for a random UUID; `contentAddressed: true` derives an immutable `<sha256>.<ext>` key. `cacheControl` sets the CDN caching of a public object (defaults: auto-minted keys immutable, named keys `public, max-age=300`).\n- `get(key)` \u2192 `Buffer` \xB7 `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```\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\n`SearchHit`: `{ score, text, 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`, `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 updates the tags in place \u2014 no re-processing.\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 \u2014 how an app builds its own timeline or index of a big source after ingest, in a background task), `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 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, bytes(), text(), json() }` and returns one of `documents([{ externalId, title, markdown, metadata?, replaces? }])`, `passthrough({ metadata? })` (ingest the raw object as-is), `skip(reason)`, or `deletes([externalId, ...])`; throwing is the object\'s error. Everything entering a mapped source is mapped \u2014 jobs, syncs and `add()` alike \u2014 so `add()` on a mapped source returns `documents` (possibly several) and throws `mapper_skipped` for an object the mapper refused. 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\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';
|
|
8547
8927
|
}
|
|
8548
8928
|
});
|
|
8549
8929
|
|
|
@@ -8780,15 +9160,15 @@ async function startMcpServer(options) {
|
|
|
8780
9160
|
return agent;
|
|
8781
9161
|
}
|
|
8782
9162
|
async function buildTools() {
|
|
8783
|
-
if (tools)
|
|
9163
|
+
if (tools) {
|
|
9164
|
+
return tools;
|
|
9165
|
+
}
|
|
8784
9166
|
const meta = await getMetadata();
|
|
8785
|
-
const stepTools = Object.entries(meta).map(
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
})
|
|
8791
|
-
);
|
|
9167
|
+
const stepTools = Object.entries(meta).map(([name, step]) => ({
|
|
9168
|
+
name,
|
|
9169
|
+
description: step.description + (step.usageNotes ? "\n\n" + step.usageNotes : ""),
|
|
9170
|
+
inputSchema: step.inputSchema
|
|
9171
|
+
}));
|
|
8792
9172
|
tools = [ASK_TOOL, ...stepTools, ...HELPER_TOOLS];
|
|
8793
9173
|
return tools;
|
|
8794
9174
|
}
|
|
@@ -8801,7 +9181,7 @@ async function startMcpServer(options) {
|
|
|
8801
9181
|
capabilities: { tools: {} },
|
|
8802
9182
|
serverInfo: {
|
|
8803
9183
|
name: "mindstudio-agent",
|
|
8804
|
-
version: "0.1.
|
|
9184
|
+
version: "0.1.129"
|
|
8805
9185
|
},
|
|
8806
9186
|
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.'
|
|
8807
9187
|
});
|
|
@@ -8818,19 +9198,14 @@ async function startMcpServer(options) {
|
|
|
8818
9198
|
let result;
|
|
8819
9199
|
if (toolName === "ask") {
|
|
8820
9200
|
const { runAsk: runAsk2 } = await init_ask().then(() => ask_exports);
|
|
8821
|
-
result = await runAsk2(
|
|
8822
|
-
args.question,
|
|
8823
|
-
options
|
|
8824
|
-
);
|
|
9201
|
+
result = await runAsk2(args.question, options);
|
|
8825
9202
|
} else if (toolName === "listActions") {
|
|
8826
9203
|
const meta = await getMetadata();
|
|
8827
9204
|
const summary = {};
|
|
8828
9205
|
for (const [name, step] of Object.entries(meta)) {
|
|
8829
9206
|
summary[name] = step.description;
|
|
8830
9207
|
}
|
|
8831
|
-
for (const [name, desc] of Object.entries(
|
|
8832
|
-
HELPER_DESCRIPTIONS
|
|
8833
|
-
)) {
|
|
9208
|
+
for (const [name, desc] of Object.entries(HELPER_DESCRIPTIONS)) {
|
|
8834
9209
|
summary[name] = desc;
|
|
8835
9210
|
}
|
|
8836
9211
|
result = summary;
|
|
@@ -8874,9 +9249,7 @@ async function startMcpServer(options) {
|
|
|
8874
9249
|
}
|
|
8875
9250
|
);
|
|
8876
9251
|
} else if (toolName === "changeName") {
|
|
8877
|
-
await getAgent().changeName(
|
|
8878
|
-
args.displayName
|
|
8879
|
-
);
|
|
9252
|
+
await getAgent().changeName(args.displayName);
|
|
8880
9253
|
result = { success: true };
|
|
8881
9254
|
} else if (toolName === "changeProfilePicture") {
|
|
8882
9255
|
await getAgent().changeProfilePicture(
|
|
@@ -8886,7 +9259,9 @@ async function startMcpServer(options) {
|
|
|
8886
9259
|
} else if (toolName === "uploadFile") {
|
|
8887
9260
|
const filePath = args.filePath;
|
|
8888
9261
|
const ext = extname(filePath).slice(1).toLowerCase();
|
|
8889
|
-
if (!ext)
|
|
9262
|
+
if (!ext) {
|
|
9263
|
+
throw new Error("Cannot determine file extension from path.");
|
|
9264
|
+
}
|
|
8890
9265
|
const content = readFileSync2(filePath);
|
|
8891
9266
|
const mimeType = MIME_TYPES[ext];
|
|
8892
9267
|
result = await getAgent().uploadFile(content, {
|
|
@@ -8914,15 +9289,10 @@ async function startMcpServer(options) {
|
|
|
8914
9289
|
sendError(id, -32602, `Unknown tool: ${toolName}`);
|
|
8915
9290
|
return;
|
|
8916
9291
|
}
|
|
8917
|
-
result = await getAgent().executeStep(
|
|
8918
|
-
step.stepType,
|
|
8919
|
-
args
|
|
8920
|
-
);
|
|
9292
|
+
result = await getAgent().executeStep(step.stepType, args);
|
|
8921
9293
|
}
|
|
8922
9294
|
sendResult(id, {
|
|
8923
|
-
content: [
|
|
8924
|
-
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
8925
|
-
]
|
|
9295
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
8926
9296
|
});
|
|
8927
9297
|
} catch (err) {
|
|
8928
9298
|
const message = err instanceof MindStudioError ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
|
|
@@ -8943,7 +9313,9 @@ async function startMcpServer(options) {
|
|
|
8943
9313
|
console.log = console.warn;
|
|
8944
9314
|
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
8945
9315
|
for await (const line of rl) {
|
|
8946
|
-
if (!line.trim())
|
|
9316
|
+
if (!line.trim()) {
|
|
9317
|
+
continue;
|
|
9318
|
+
}
|
|
8947
9319
|
try {
|
|
8948
9320
|
const msg = JSON.parse(line);
|
|
8949
9321
|
await handleMessage(msg);
|
|
@@ -9353,10 +9725,18 @@ function parseJson5(input) {
|
|
|
9353
9725
|
return JSON.parse(s);
|
|
9354
9726
|
}
|
|
9355
9727
|
function coerce(value) {
|
|
9356
|
-
if (typeof value !== "string")
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
if (value
|
|
9728
|
+
if (typeof value !== "string") {
|
|
9729
|
+
return value;
|
|
9730
|
+
}
|
|
9731
|
+
if (value === "true") {
|
|
9732
|
+
return true;
|
|
9733
|
+
}
|
|
9734
|
+
if (value === "false") {
|
|
9735
|
+
return false;
|
|
9736
|
+
}
|
|
9737
|
+
if (value !== "" && !isNaN(Number(value))) {
|
|
9738
|
+
return Number(value);
|
|
9739
|
+
}
|
|
9360
9740
|
if (value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]")) {
|
|
9361
9741
|
try {
|
|
9362
9742
|
return JSON.parse(value);
|
|
@@ -9372,8 +9752,12 @@ function levenshtein(a, b) {
|
|
|
9372
9752
|
{ length: m + 1 },
|
|
9373
9753
|
() => Array(n + 1).fill(0)
|
|
9374
9754
|
);
|
|
9375
|
-
for (let i = 0; i <= m; i++)
|
|
9376
|
-
|
|
9755
|
+
for (let i = 0; i <= m; i++) {
|
|
9756
|
+
dp[i][0] = i;
|
|
9757
|
+
}
|
|
9758
|
+
for (let j = 0; j <= n; j++) {
|
|
9759
|
+
dp[0][j] = j;
|
|
9760
|
+
}
|
|
9377
9761
|
for (let i = 1; i <= m; i++) {
|
|
9378
9762
|
for (let j = 1; j <= n; j++) {
|
|
9379
9763
|
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
@@ -9400,9 +9784,13 @@ async function readStdin() {
|
|
|
9400
9784
|
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
9401
9785
|
}
|
|
9402
9786
|
function resolveMethodOrFail(name, metadataKeys) {
|
|
9403
|
-
if (metadataKeys.has(name))
|
|
9787
|
+
if (metadataKeys.has(name)) {
|
|
9788
|
+
return name;
|
|
9789
|
+
}
|
|
9404
9790
|
const camel = kebabToCamel2(name);
|
|
9405
|
-
if (metadataKeys.has(camel))
|
|
9791
|
+
if (metadataKeys.has(camel)) {
|
|
9792
|
+
return camel;
|
|
9793
|
+
}
|
|
9406
9794
|
const kebab = name.includes("-") ? name : camelToKebab(name);
|
|
9407
9795
|
let bestDist = Infinity;
|
|
9408
9796
|
let bestMatch = "";
|
|
@@ -9467,12 +9855,16 @@ async function cmdInfo(rawMethod) {
|
|
|
9467
9855
|
out.push("");
|
|
9468
9856
|
out.push(` ${camelToKebab(method)}`);
|
|
9469
9857
|
out.push("");
|
|
9470
|
-
if (meta.description)
|
|
9858
|
+
if (meta.description) {
|
|
9859
|
+
out.push(` ${meta.description}`);
|
|
9860
|
+
}
|
|
9471
9861
|
if (meta.usageNotes) {
|
|
9472
9862
|
out.push("");
|
|
9473
9863
|
for (const line of meta.usageNotes.split("\n")) {
|
|
9474
9864
|
const trimmed = line.trim();
|
|
9475
|
-
if (trimmed)
|
|
9865
|
+
if (trimmed) {
|
|
9866
|
+
out.push(` ${trimmed}`);
|
|
9867
|
+
}
|
|
9476
9868
|
}
|
|
9477
9869
|
}
|
|
9478
9870
|
const schema = meta.inputSchema;
|
|
@@ -9503,12 +9895,18 @@ async function cmdInfo(rawMethod) {
|
|
|
9503
9895
|
process.stderr.write(out.join("\n") + "\n");
|
|
9504
9896
|
}
|
|
9505
9897
|
function formatPropType(prop) {
|
|
9506
|
-
if (prop.enum)
|
|
9898
|
+
if (prop.enum) {
|
|
9507
9899
|
return prop.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
9508
|
-
|
|
9509
|
-
if (prop.type === "
|
|
9510
|
-
|
|
9900
|
+
}
|
|
9901
|
+
if (prop.type === "array") {
|
|
9902
|
+
return "array";
|
|
9903
|
+
}
|
|
9904
|
+
if (prop.type === "object") {
|
|
9905
|
+
return "object";
|
|
9906
|
+
}
|
|
9907
|
+
if (typeof prop.type === "string") {
|
|
9511
9908
|
return prop.type === "integer" ? "number" : prop.type;
|
|
9909
|
+
}
|
|
9512
9910
|
return "string";
|
|
9513
9911
|
}
|
|
9514
9912
|
async function cmdExec(method, input, options) {
|
|
@@ -9531,15 +9929,18 @@ async function cmdExec(method, input, options) {
|
|
|
9531
9929
|
if (options.jsonLogs) {
|
|
9532
9930
|
onLog = (log) => {
|
|
9533
9931
|
process.stderr.write(
|
|
9534
|
-
JSON.stringify({
|
|
9932
|
+
JSON.stringify({
|
|
9933
|
+
type: "log",
|
|
9934
|
+
value: log.value,
|
|
9935
|
+
tag: log.tag,
|
|
9936
|
+
ts: log.ts
|
|
9937
|
+
}) + "\n"
|
|
9535
9938
|
);
|
|
9536
9939
|
};
|
|
9537
9940
|
} else if (process.stderr.isTTY) {
|
|
9538
9941
|
onLog = (log) => {
|
|
9539
|
-
process.stderr.write(
|
|
9540
|
-
|
|
9541
|
-
`
|
|
9542
|
-
);
|
|
9942
|
+
process.stderr.write(` ${ansi2.cyan("\u27E1")} ${ansi2.gray(log.value)}
|
|
9943
|
+
`);
|
|
9543
9944
|
};
|
|
9544
9945
|
}
|
|
9545
9946
|
const result = await agent.executeStep(meta.stepType, input, {
|
|
@@ -9557,7 +9958,9 @@ async function cmdExec(method, input, options) {
|
|
|
9557
9958
|
} else if (options.noMeta) {
|
|
9558
9959
|
const filtered = {};
|
|
9559
9960
|
for (const [k, v] of Object.entries(result)) {
|
|
9560
|
-
if (!k.startsWith("$"))
|
|
9961
|
+
if (!k.startsWith("$")) {
|
|
9962
|
+
filtered[k] = v;
|
|
9963
|
+
}
|
|
9561
9964
|
}
|
|
9562
9965
|
process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
|
|
9563
9966
|
} else {
|
|
@@ -9677,7 +10080,9 @@ async function cmdRun(appId, variables, options) {
|
|
|
9677
10080
|
} else if (options.noMeta) {
|
|
9678
10081
|
const filtered = {};
|
|
9679
10082
|
for (const [k, v] of Object.entries(obj)) {
|
|
9680
|
-
if (!k.startsWith("$"))
|
|
10083
|
+
if (!k.startsWith("$")) {
|
|
10084
|
+
filtered[k] = v;
|
|
10085
|
+
}
|
|
9681
10086
|
}
|
|
9682
10087
|
process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
|
|
9683
10088
|
} else {
|
|
@@ -9703,22 +10108,20 @@ Example: mindstudio batch '[{"stepType":"generateImage","step":{"prompt":"a cat"
|
|
|
9703
10108
|
const metaByName = new Map(
|
|
9704
10109
|
Object.entries(stepMetadata2).map(([name, m]) => [name, m])
|
|
9705
10110
|
);
|
|
9706
|
-
const steps = input.map(
|
|
9707
|
-
(item
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
meta = metaByName.get(camel);
|
|
9715
|
-
}
|
|
9716
|
-
if (meta) {
|
|
9717
|
-
return { stepType: meta.stepType, step: item.step };
|
|
9718
|
-
}
|
|
9719
|
-
return { stepType: item.stepType, step: item.step };
|
|
10111
|
+
const steps = input.map((item, i) => {
|
|
10112
|
+
let meta = metaByName.get(item.stepType);
|
|
10113
|
+
if (!meta) {
|
|
10114
|
+
const camel = item.stepType.replace(
|
|
10115
|
+
/-([a-z])/g,
|
|
10116
|
+
(_, c) => c.toUpperCase()
|
|
10117
|
+
);
|
|
10118
|
+
meta = metaByName.get(camel);
|
|
9720
10119
|
}
|
|
9721
|
-
|
|
10120
|
+
if (meta) {
|
|
10121
|
+
return { stepType: meta.stepType, step: item.step };
|
|
10122
|
+
}
|
|
10123
|
+
return { stepType: item.stepType, step: item.step };
|
|
10124
|
+
});
|
|
9722
10125
|
const agent = await createAgent(options);
|
|
9723
10126
|
const result = await agent.executeStepBatch(steps, {
|
|
9724
10127
|
appId: options.appId,
|
|
@@ -9748,7 +10151,11 @@ var MIME_TYPES2 = {
|
|
|
9748
10151
|
};
|
|
9749
10152
|
async function cmdUpload(filePath, options) {
|
|
9750
10153
|
const ext = extname2(filePath).slice(1).toLowerCase();
|
|
9751
|
-
if (!ext)
|
|
10154
|
+
if (!ext) {
|
|
10155
|
+
fatal(
|
|
10156
|
+
"Cannot determine file extension. Please provide a file with an extension."
|
|
10157
|
+
);
|
|
10158
|
+
}
|
|
9752
10159
|
const content = readFileSync3(filePath);
|
|
9753
10160
|
const mimeType = MIME_TYPES2[ext];
|
|
9754
10161
|
const { MindStudioAgent: MindStudioAgent2 } = await init_client().then(() => client_exports);
|
|
@@ -9780,14 +10187,20 @@ function isNewerVersion(current, latest) {
|
|
|
9780
10187
|
for (let i = 0; i < Math.max(c.length, l.length); i++) {
|
|
9781
10188
|
const cv = c[i] ?? 0;
|
|
9782
10189
|
const lv = l[i] ?? 0;
|
|
9783
|
-
if (lv > cv)
|
|
9784
|
-
|
|
10190
|
+
if (lv > cv) {
|
|
10191
|
+
return true;
|
|
10192
|
+
}
|
|
10193
|
+
if (lv < cv) {
|
|
10194
|
+
return false;
|
|
10195
|
+
}
|
|
9785
10196
|
}
|
|
9786
10197
|
return false;
|
|
9787
10198
|
}
|
|
9788
10199
|
async function checkForUpdate() {
|
|
9789
|
-
const currentVersion = "0.1.
|
|
9790
|
-
if (!currentVersion)
|
|
10200
|
+
const currentVersion = "0.1.129";
|
|
10201
|
+
if (!currentVersion) {
|
|
10202
|
+
return null;
|
|
10203
|
+
}
|
|
9791
10204
|
try {
|
|
9792
10205
|
const { loadConfig: loadConfig2, saveConfig: saveConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9793
10206
|
const config = loadConfig2();
|
|
@@ -9801,10 +10214,14 @@ async function checkForUpdate() {
|
|
|
9801
10214
|
"https://registry.npmjs.org/@mindstudio-ai/agent/latest",
|
|
9802
10215
|
{ signal: AbortSignal.timeout(5e3) }
|
|
9803
10216
|
);
|
|
9804
|
-
if (!res.ok)
|
|
10217
|
+
if (!res.ok) {
|
|
10218
|
+
return null;
|
|
10219
|
+
}
|
|
9805
10220
|
const data = await res.json();
|
|
9806
10221
|
const latestVersion = data.version;
|
|
9807
|
-
if (!latestVersion)
|
|
10222
|
+
if (!latestVersion) {
|
|
10223
|
+
return null;
|
|
10224
|
+
}
|
|
9808
10225
|
saveConfig2({
|
|
9809
10226
|
...config,
|
|
9810
10227
|
_updateCheck: { latestVersion, checkedAt: Date.now() }
|
|
@@ -9815,7 +10232,7 @@ async function checkForUpdate() {
|
|
|
9815
10232
|
}
|
|
9816
10233
|
}
|
|
9817
10234
|
function printUpdateNotice(latestVersion) {
|
|
9818
|
-
const currentVersion = "0.1.
|
|
10235
|
+
const currentVersion = "0.1.129";
|
|
9819
10236
|
process.stderr.write(
|
|
9820
10237
|
`
|
|
9821
10238
|
${ansi2.cyanBright("Update available")} ${ansi2.gray(currentVersion + " \u2192")} ${ansi2.cyanBold(latestVersion)}
|
|
@@ -9828,7 +10245,7 @@ function isStandaloneBinary() {
|
|
|
9828
10245
|
return !argv1.includes("node_modules");
|
|
9829
10246
|
}
|
|
9830
10247
|
async function cmdUpdate() {
|
|
9831
|
-
const currentVersion = "0.1.
|
|
10248
|
+
const currentVersion = "0.1.129";
|
|
9832
10249
|
process.stderr.write(
|
|
9833
10250
|
` ${ansi2.gray("Current version:")} ${currentVersion}
|
|
9834
10251
|
`
|
|
@@ -9918,9 +10335,13 @@ function printLogo() {
|
|
|
9918
10335
|
}
|
|
9919
10336
|
function openBrowser(url) {
|
|
9920
10337
|
try {
|
|
9921
|
-
if (process.platform === "darwin")
|
|
9922
|
-
|
|
9923
|
-
else
|
|
10338
|
+
if (process.platform === "darwin") {
|
|
10339
|
+
execSync(`open "${url}"`);
|
|
10340
|
+
} else if (process.platform === "win32") {
|
|
10341
|
+
execSync(`start "" "${url}"`);
|
|
10342
|
+
} else {
|
|
10343
|
+
execSync(`xdg-open "${url}"`);
|
|
10344
|
+
}
|
|
9924
10345
|
} catch {
|
|
9925
10346
|
}
|
|
9926
10347
|
}
|
|
@@ -9943,7 +10364,9 @@ function waitForKeypress() {
|
|
|
9943
10364
|
});
|
|
9944
10365
|
}
|
|
9945
10366
|
function maskKey(key) {
|
|
9946
|
-
if (key.length <= 8)
|
|
10367
|
+
if (key.length <= 8) {
|
|
10368
|
+
return "****";
|
|
10369
|
+
}
|
|
9947
10370
|
return key.slice(0, 4) + "..." + key.slice(-4);
|
|
9948
10371
|
}
|
|
9949
10372
|
var DEFAULT_BASE_URL3 = "https://v1.mindstudio-api.com";
|
|
@@ -9963,7 +10386,7 @@ async function cmdLogin(options) {
|
|
|
9963
10386
|
process.stderr.write("\n");
|
|
9964
10387
|
printLogo();
|
|
9965
10388
|
process.stderr.write("\n");
|
|
9966
|
-
const ver = "0.1.
|
|
10389
|
+
const ver = "0.1.129";
|
|
9967
10390
|
process.stderr.write(
|
|
9968
10391
|
` ${ansi2.bold("MindStudio Agent")} ${ver ? " " + ansi2.gray("v" + ver) : ""}
|
|
9969
10392
|
`
|
|
@@ -10110,7 +10533,9 @@ async function cmdWhoami(options) {
|
|
|
10110
10533
|
}
|
|
10111
10534
|
process.stderr.write(` ${ansi2.gray("Auth:")} ${source}
|
|
10112
10535
|
`);
|
|
10113
|
-
for (const line of detail)
|
|
10536
|
+
for (const line of detail) {
|
|
10537
|
+
process.stderr.write(line + "\n");
|
|
10538
|
+
}
|
|
10114
10539
|
process.stderr.write(` ${ansi2.gray("Verifying...")} `);
|
|
10115
10540
|
try {
|
|
10116
10541
|
const { MindStudioAgent: MindStudioAgent2 } = await init_client().then(() => client_exports);
|
|
@@ -10126,10 +10551,8 @@ async function cmdWhoami(options) {
|
|
|
10126
10551
|
);
|
|
10127
10552
|
process.stderr.write(` ${ansi2.bold("User")}
|
|
10128
10553
|
`);
|
|
10129
|
-
process.stderr.write(
|
|
10130
|
-
|
|
10131
|
-
`
|
|
10132
|
-
);
|
|
10554
|
+
process.stderr.write(` ${ansi2.gray("Name:")} ${info.displayName}
|
|
10555
|
+
`);
|
|
10133
10556
|
process.stderr.write(
|
|
10134
10557
|
` ${ansi2.gray("ID:")} ${ansi2.gray(info.userId)}
|
|
10135
10558
|
`
|
|
@@ -10137,10 +10560,8 @@ async function cmdWhoami(options) {
|
|
|
10137
10560
|
process.stderr.write(`
|
|
10138
10561
|
${ansi2.bold("Organization")}
|
|
10139
10562
|
`);
|
|
10140
|
-
process.stderr.write(
|
|
10141
|
-
|
|
10142
|
-
`
|
|
10143
|
-
);
|
|
10563
|
+
process.stderr.write(` ${ansi2.gray("Name:")} ${info.organizationName}
|
|
10564
|
+
`);
|
|
10144
10565
|
process.stderr.write(
|
|
10145
10566
|
` ${ansi2.gray("ID:")} ${ansi2.gray(info.organizationId)}
|
|
10146
10567
|
`
|
|
@@ -10153,10 +10574,7 @@ async function cmdWhoami(options) {
|
|
|
10153
10574
|
4,
|
|
10154
10575
|
...info.members.map((m) => m.displayName.length)
|
|
10155
10576
|
);
|
|
10156
|
-
const roleWidth = Math.max(
|
|
10157
|
-
4,
|
|
10158
|
-
...info.members.map((m) => m.role.length)
|
|
10159
|
-
);
|
|
10577
|
+
const roleWidth = Math.max(4, ...info.members.map((m) => m.role.length));
|
|
10160
10578
|
process.stderr.write(
|
|
10161
10579
|
` ${ansi2.gray("Name".padEnd(nameWidth))} ${ansi2.gray("Role".padEnd(roleWidth))} ${ansi2.gray("Type")}
|
|
10162
10580
|
`
|
|
@@ -10186,8 +10604,12 @@ function parseStepFlags(argv) {
|
|
|
10186
10604
|
const result = {};
|
|
10187
10605
|
for (let i = 0; i < argv.length; i++) {
|
|
10188
10606
|
const arg = argv[i];
|
|
10189
|
-
if (!arg.startsWith("--"))
|
|
10190
|
-
|
|
10607
|
+
if (!arg.startsWith("--")) {
|
|
10608
|
+
continue;
|
|
10609
|
+
}
|
|
10610
|
+
if (GLOBAL_BOOLEAN_FLAGS.has(arg)) {
|
|
10611
|
+
continue;
|
|
10612
|
+
}
|
|
10191
10613
|
if (GLOBAL_STRING_FLAGS.has(arg)) {
|
|
10192
10614
|
i++;
|
|
10193
10615
|
continue;
|
|
@@ -10221,7 +10643,9 @@ function findMethodSplit(argv) {
|
|
|
10221
10643
|
for (let i = 0; i < argv.length; i++) {
|
|
10222
10644
|
const arg = argv[i];
|
|
10223
10645
|
if (arg.startsWith("--")) {
|
|
10224
|
-
if (GLOBAL_STRING_FLAGS.has(arg))
|
|
10646
|
+
if (GLOBAL_STRING_FLAGS.has(arg)) {
|
|
10647
|
+
i++;
|
|
10648
|
+
}
|
|
10225
10649
|
continue;
|
|
10226
10650
|
}
|
|
10227
10651
|
if (arg === "run") {
|
|
@@ -10235,7 +10659,9 @@ function findMethodSplit(argv) {
|
|
|
10235
10659
|
for (let i = startIdx; i < argv.length; i++) {
|
|
10236
10660
|
const arg = argv[i];
|
|
10237
10661
|
if (arg.startsWith("--")) {
|
|
10238
|
-
if (GLOBAL_STRING_FLAGS.has(arg))
|
|
10662
|
+
if (GLOBAL_STRING_FLAGS.has(arg)) {
|
|
10663
|
+
i++;
|
|
10664
|
+
}
|
|
10239
10665
|
continue;
|
|
10240
10666
|
}
|
|
10241
10667
|
if (hasRun || i === startIdx) {
|
|
@@ -10289,9 +10715,7 @@ async function main() {
|
|
|
10289
10715
|
const updatePromise = command !== "mcp" && command !== "login" && command !== "update" ? checkForUpdate() : Promise.resolve(null);
|
|
10290
10716
|
try {
|
|
10291
10717
|
if (command === "version" || command === "-v") {
|
|
10292
|
-
process.stdout.write(
|
|
10293
|
-
"0.1.127\n"
|
|
10294
|
-
);
|
|
10718
|
+
process.stdout.write("0.1.129\n");
|
|
10295
10719
|
return;
|
|
10296
10720
|
}
|
|
10297
10721
|
if (command === "login") {
|
|
@@ -10415,7 +10839,7 @@ async function main() {
|
|
|
10415
10839
|
}
|
|
10416
10840
|
if (command === "run-agent") {
|
|
10417
10841
|
const appId = positionals[1];
|
|
10418
|
-
if (!appId)
|
|
10842
|
+
if (!appId) {
|
|
10419
10843
|
usageBlock([
|
|
10420
10844
|
"run-agent \u2014 Run a pre-built agent and wait for the result",
|
|
10421
10845
|
"",
|
|
@@ -10435,6 +10859,7 @@ async function main() {
|
|
|
10435
10859
|
"",
|
|
10436
10860
|
'Tip: run "mindstudio agents" to list available agent IDs.'
|
|
10437
10861
|
]);
|
|
10862
|
+
}
|
|
10438
10863
|
const runArgv = process.argv.slice(process.argv.indexOf("run-agent") + 2);
|
|
10439
10864
|
const stepArgs = [];
|
|
10440
10865
|
for (let i = 0; i < runArgv.length; i++) {
|
|
@@ -10482,7 +10907,7 @@ async function main() {
|
|
|
10482
10907
|
}
|
|
10483
10908
|
if (command === "upload" || command === "upload-file") {
|
|
10484
10909
|
const filePath = positionals[1];
|
|
10485
|
-
if (!filePath)
|
|
10910
|
+
if (!filePath) {
|
|
10486
10911
|
usageBlock([
|
|
10487
10912
|
"upload \u2014 Upload a file to the MindStudio CDN (alias: upload-file)",
|
|
10488
10913
|
"",
|
|
@@ -10497,6 +10922,7 @@ async function main() {
|
|
|
10497
10922
|
" mindstudio upload photo.png",
|
|
10498
10923
|
" mindstudio upload-file /path/to/document.pdf"
|
|
10499
10924
|
]);
|
|
10925
|
+
}
|
|
10500
10926
|
await cmdUpload(filePath, {
|
|
10501
10927
|
apiKey: values["api-key"],
|
|
10502
10928
|
baseUrl: values["base-url"]
|
|
@@ -10512,7 +10938,7 @@ async function main() {
|
|
|
10512
10938
|
let summary = false;
|
|
10513
10939
|
if (command === "list-models-by-type" || command === "list-models-summary-by-type") {
|
|
10514
10940
|
type = positionals[1];
|
|
10515
|
-
if (!type)
|
|
10941
|
+
if (!type) {
|
|
10516
10942
|
usageBlock([
|
|
10517
10943
|
`${command} \u2014 List AI models filtered by type`,
|
|
10518
10944
|
"",
|
|
@@ -10527,14 +10953,19 @@ async function main() {
|
|
|
10527
10953
|
` mindstudio ${command} image_generation`,
|
|
10528
10954
|
` mindstudio ${command} llm_chat`
|
|
10529
10955
|
]);
|
|
10956
|
+
}
|
|
10530
10957
|
}
|
|
10531
10958
|
if (command === "list-models-summary" || command === "list-models-summary-by-type") {
|
|
10532
10959
|
summary = true;
|
|
10533
10960
|
}
|
|
10534
10961
|
if (command === "list-models") {
|
|
10535
10962
|
const typeFlag = values.type;
|
|
10536
|
-
if (typeFlag)
|
|
10537
|
-
|
|
10963
|
+
if (typeFlag) {
|
|
10964
|
+
type = typeFlag;
|
|
10965
|
+
}
|
|
10966
|
+
if (values.summary) {
|
|
10967
|
+
summary = true;
|
|
10968
|
+
}
|
|
10538
10969
|
}
|
|
10539
10970
|
await cmdListModels({ ...authOpts, type, summary });
|
|
10540
10971
|
return;
|
|
@@ -10562,7 +10993,7 @@ async function main() {
|
|
|
10562
10993
|
}
|
|
10563
10994
|
if (command === "estimate-cost") {
|
|
10564
10995
|
const stepMethod = positionals[1];
|
|
10565
|
-
if (!stepMethod)
|
|
10996
|
+
if (!stepMethod) {
|
|
10566
10997
|
usageBlock([
|
|
10567
10998
|
"estimate-cost \u2014 Estimate the cost of an action before running it",
|
|
10568
10999
|
"",
|
|
@@ -10575,6 +11006,7 @@ async function main() {
|
|
|
10575
11006
|
"",
|
|
10576
11007
|
'Tip: run "mindstudio list-actions" to see available actions.'
|
|
10577
11008
|
]);
|
|
11009
|
+
}
|
|
10578
11010
|
const allKeys2 = await getAllMethodKeys();
|
|
10579
11011
|
const resolvedMethod = resolveMethodOrFail(stepMethod, allKeys2);
|
|
10580
11012
|
const { stepMetadata: stepMetadata2 } = await Promise.resolve().then(() => (init_metadata(), metadata_exports));
|
|
@@ -10599,7 +11031,7 @@ async function main() {
|
|
|
10599
11031
|
}
|
|
10600
11032
|
if (command === "change-name") {
|
|
10601
11033
|
const name = positionals[1];
|
|
10602
|
-
if (!name)
|
|
11034
|
+
if (!name) {
|
|
10603
11035
|
usageBlock([
|
|
10604
11036
|
"change-name \u2014 Update your display name",
|
|
10605
11037
|
"",
|
|
@@ -10609,6 +11041,7 @@ async function main() {
|
|
|
10609
11041
|
"Examples:",
|
|
10610
11042
|
' mindstudio change-name "My Agent"'
|
|
10611
11043
|
]);
|
|
11044
|
+
}
|
|
10612
11045
|
await cmdChangeName(name, {
|
|
10613
11046
|
apiKey: values["api-key"],
|
|
10614
11047
|
baseUrl: values["base-url"]
|
|
@@ -10617,7 +11050,7 @@ async function main() {
|
|
|
10617
11050
|
}
|
|
10618
11051
|
if (command === "change-profile-picture") {
|
|
10619
11052
|
const url = positionals[1];
|
|
10620
|
-
if (!url)
|
|
11053
|
+
if (!url) {
|
|
10621
11054
|
usageBlock([
|
|
10622
11055
|
"change-profile-picture \u2014 Update your profile picture",
|
|
10623
11056
|
"",
|
|
@@ -10629,6 +11062,7 @@ async function main() {
|
|
|
10629
11062
|
"",
|
|
10630
11063
|
'Tip: use "mindstudio upload" to host an image first.'
|
|
10631
11064
|
]);
|
|
11065
|
+
}
|
|
10632
11066
|
await cmdChangeProfilePicture(url, {
|
|
10633
11067
|
apiKey: values["api-key"],
|
|
10634
11068
|
baseUrl: values["base-url"]
|
|
@@ -10645,7 +11079,7 @@ async function main() {
|
|
|
10645
11079
|
}
|
|
10646
11080
|
if (command === "info") {
|
|
10647
11081
|
const rawMethod2 = positionals[1];
|
|
10648
|
-
if (!rawMethod2)
|
|
11082
|
+
if (!rawMethod2) {
|
|
10649
11083
|
usageBlock([
|
|
10650
11084
|
"info \u2014 Show action details and parameters",
|
|
10651
11085
|
"",
|
|
@@ -10661,11 +11095,12 @@ async function main() {
|
|
|
10661
11095
|
"",
|
|
10662
11096
|
'Tip: run "mindstudio list-actions" to see available actions.'
|
|
10663
11097
|
]);
|
|
11098
|
+
}
|
|
10664
11099
|
await cmdInfo(rawMethod2);
|
|
10665
11100
|
return;
|
|
10666
11101
|
}
|
|
10667
11102
|
const split = findMethodSplit(process.argv.slice(2));
|
|
10668
|
-
if (!split)
|
|
11103
|
+
if (!split) {
|
|
10669
11104
|
usageBlock([
|
|
10670
11105
|
"Run an action directly",
|
|
10671
11106
|
"",
|
|
@@ -10688,6 +11123,7 @@ async function main() {
|
|
|
10688
11123
|
"",
|
|
10689
11124
|
'Tip: run "mindstudio list-actions" to see available actions.'
|
|
10690
11125
|
]);
|
|
11126
|
+
}
|
|
10691
11127
|
const { rawMethod, stepArgv } = split;
|
|
10692
11128
|
const allKeys = await getAllMethodKeys();
|
|
10693
11129
|
const method = resolveMethodOrFail(rawMethod, allKeys);
|
|
@@ -10728,7 +11164,9 @@ async function main() {
|
|
|
10728
11164
|
fatal(message);
|
|
10729
11165
|
} finally {
|
|
10730
11166
|
const latestVersion = await updatePromise;
|
|
10731
|
-
if (latestVersion)
|
|
11167
|
+
if (latestVersion) {
|
|
11168
|
+
printUpdateNotice(latestVersion);
|
|
11169
|
+
}
|
|
10732
11170
|
}
|
|
10733
11171
|
}
|
|
10734
11172
|
main();
|