@mindstudio-ai/agent 0.1.163 → 0.1.165

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/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ What changed in `@mindstudio-ai/agent`, newest first, each version under its own heading. An entry says what's new, what's fixed, and what changed behavior, with what an app's code should be checked for. An entry whose version npm doesn't have yet is still open: a later change adds to it.
4
+
5
+ Versions before 0.1.163 aren't recorded here.
6
+
7
+ ## 0.1.165
8
+
9
+ ### New
10
+
11
+ - The CLI can log what each call costs. When `MINDSTUDIO_USAGE_LOG` names a file, every billed call from an action, `batch` (a line per step) or `ask` appends one JSON line to it: `{ ts, action, billingCost, billingEvents?, durationMs }`, with `billingCost` in nano-dollars. It's for a tool that runs the CLI and keeps its own record of spend, whatever happens to the command's output. App code isn't affected.
12
+
13
+ ## 0.1.164
14
+
15
+ ### New
16
+
17
+ - The package carries this changelog, `CHANGELOG.md`, from this version on. Its entries start at 0.1.163.
18
+
19
+ ### Fixed
20
+
21
+ - Filters on a column typed with a named type compile again. A column like `category: DocumentCategory` is stored as a json column. In 0.1.163, `===`, `!==` and `[...].includes(o.category)` with strings on it read the whole table into JS, and `o.category.includes('x')` failed the query with `malformed JSON` once a row held a plain string.
22
+ - `o.field == null` on a json column also matches a row whose text is `null`, which reads back as null. That happens when the string `'null'` was written.
23
+
24
+ ## 0.1.163
25
+
26
+ Published by release v1.0.2832.
27
+
28
+ ### Changed behavior
29
+
30
+ A filter that compiles to SQL now selects exactly the rows its function would in JavaScript, NULL columns included. Before, some compiled to SQL that answered differently, and which answer an app got depended on whether the filter compiled.
31
+
32
+ - `!==` and `!=` keep rows where the column is NULL, as in JS (`null !== 'archived'` is true). They used to drop them. That includes `removeAll`, which now deletes those rows too. Check: a `filter`, `count` or `removeAll` with `!==` on a column that can be empty, where empty rows should be left out. Add `o.status != null &&`.
33
+ - `<`, `<=`, `>` and `>=` give JS's answer for a NULL column: `o.n < 5` keeps a row with no `n`, since `null < 5` is true in JS. So does `!(…)`: `!(o.status === 'a')` keeps empty rows. Check: comparisons on a nullable number column where empty rows should be left out.
34
+ - `o.text.includes('x')` is case-sensitive, as JS's `includes` is, and `_` and `%` in the needle are literal characters. It used to be a case-insensitive SQL `LIKE`, in which a needle holding `_` or `%` matched nothing. Check: a search box written as `o.name.includes(q)` that relies on case-insensitive matching. Keep a lowercased copy of the field and compare `o.nameLower.includes(q.toLowerCase())` with `q` passed in bindings, or use `db.sql()` with `LIKE` (and `ESCAPE`).
35
+ - `o.tags.includes('x')` on a json array column is array membership, as in JS. It used to be a substring of the column's JSON text, so `'x'` also matched `'xy'`.
36
+ - A bare field reads by JS truthiness for its type: `o.name` is false for `''`, `o.count` for `0`, `!o.flag` is true for false and for an empty boolean. They used to compile to `= 1` and `= 0`, which was wrong for text and number columns.
37
+ - A computed `sortBy` (anything but `o => o.field`, such as `o => o.meta.date` or `o => o.name.toLowerCase()`) sorts. It used to be ignored whenever `take()` or `skip()` applied, so `.sortBy(computed).take(10)` returned 10 arbitrary rows. The rows are now sorted in JS after the filter runs in SQL, so `take()` reads every matching row first, and on a large table that can reach `result_too_large`. Check: a computed `sortBy` with `take()` on a table that grows. Sort by a column (`o => o.createdAt`) where you can.
38
+ - With no `sortBy`, `reverse()` and `last()` order by the reverse of the order rows were written in. They used to do nothing, so `last()` returned the first row.
39
+ - A filter SQL can't answer exactly runs in JS over every row instead, with a warning in the logs: a value of another type than its column (`o.amount > '100'`), a relational or loose comparison inside a JSON field (`o.stats.score > 50`, `o.meta.kind == 'x'`), and `.length`. The answer is right; the cost is reading the whole table.
40
+
41
+ ### New
42
+
43
+ - A comparison with a constant expression compiles: `o.createdAt > db.ago(db.days(30))`, `o.createdAt > Date.now() - 90 * 24 * 60 * 60 * 1000`, `o.amount > 50 * 100`. These used to read the whole table into JS.
44
+ - Comparisons compile either way round: `5000 < o.amount`.
45
+ - `.select(...columns)` on a query or table returns only those columns (`SELECT id, name`). Filters and sorts still see the whole row.
46
+ - Reads a method starts together against one database, such as a `Promise.all` of queries, go to the platform as one round trip. Each gets its own results, and identical reads in flight share one answer. `db.batch()` is still the way to run reads and writes together, in order, as one transaction.
47
+ - Every value in a compiled filter is a bound parameter, an `IN` list included, so a query's statement doesn't change with its values.
48
+
49
+ ### Fixed
50
+
51
+ - Filters in a deployed method's bundled code compile. esbuild prints `5000` as `5e3` and `'Zürich'` as `"Z\xFCrich"`, which the compiler couldn't read: a round number of 1,000 or more read the whole table into JS, and a string with a non-ASCII character or an escape matched nothing at all.
52
+ - `skip()` without `take()` failed with a SQL error.
53
+ - `[null, 'a'].includes(o.x)` didn't match rows where the field is empty.
package/dist/cli.js CHANGED
@@ -9,6 +9,31 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/usageLog.ts
13
+ import { appendFileSync } from "fs";
14
+ function logUsage(entry) {
15
+ const file = process.env.MINDSTUDIO_USAGE_LOG;
16
+ if (!file || typeof entry.billingCost !== "number") {
17
+ return;
18
+ }
19
+ const line = {
20
+ ts: Date.now(),
21
+ action: entry.action,
22
+ billingCost: entry.billingCost,
23
+ ...entry.billingEvents?.length && { billingEvents: entry.billingEvents },
24
+ durationMs: Date.now() - entry.startedAt
25
+ };
26
+ try {
27
+ appendFileSync(file, JSON.stringify(line) + "\n");
28
+ } catch {
29
+ }
30
+ }
31
+ var init_usageLog = __esm({
32
+ "src/usageLog.ts"() {
33
+ "use strict";
34
+ }
35
+ });
36
+
12
37
  // src/generated/metadata.ts
13
38
  var metadata_exports = {};
14
39
  __export(metadata_exports, {
@@ -2413,6 +2438,25 @@ function nestedComparison(node, negated, params) {
2413
2438
  function withNulls(core, field, nullsMatch) {
2414
2439
  return nullsMatch ? `(${core} OR ${field.sql} IS NULL)` : core;
2415
2440
  }
2441
+ function readsBackAsWritten(value) {
2442
+ if (typeof value !== "string") {
2443
+ return false;
2444
+ }
2445
+ try {
2446
+ JSON.parse(value);
2447
+ return false;
2448
+ } catch {
2449
+ return true;
2450
+ }
2451
+ }
2452
+ function jsonColumnIn(field, values, negated, params) {
2453
+ params.push(JSON.stringify(values.flatMap((v) => [v, JSON.stringify(v)])));
2454
+ return withNulls(
2455
+ `${field.sql} ${negated ? "NOT IN" : "IN"} (SELECT value FROM json_each(?))`,
2456
+ field,
2457
+ negated
2458
+ );
2459
+ }
2416
2460
  function compileNode(node, negated, columns, params) {
2417
2461
  const column = (field) => field.nested ? void 0 : columns.find((c) => c.name === field.column);
2418
2462
  const stored = (field, value) => field.nested ? serializeColumnParam(value, "", []) : serializeColumnParam(value, field.column, columns);
@@ -2430,6 +2474,10 @@ function compileNode(node, negated, columns, params) {
2430
2474
  return nestedComparison(node, negated, params);
2431
2475
  }
2432
2476
  const type = column(node.field)?.type;
2477
+ if (type === "json") {
2478
+ const strictEquals = node.op === "===" ? !negated : node.op === "!==" ? negated : null;
2479
+ return strictEquals !== null && readsBackAsWritten(node.value) ? jsonColumnIn(node.field, [node.value], !strictEquals, params) : null;
2480
+ }
2433
2481
  if (!type || typeof node.value !== VALUE_KIND[type]) {
2434
2482
  return null;
2435
2483
  }
@@ -2450,10 +2498,16 @@ function compileNode(node, negated, columns, params) {
2450
2498
  return null;
2451
2499
  }
2452
2500
  const isNull = negated ? !node.isNull : node.isNull;
2501
+ if (column(node.field)?.type === "json") {
2502
+ return isNull ? `(${node.field.sql} IS NULL OR ${node.field.sql} = 'null')` : `(${node.field.sql} IS NOT NULL AND ${node.field.sql} <> 'null')`;
2503
+ }
2453
2504
  return `${node.field.sql} ${isNull ? "IS NULL" : "IS NOT NULL"}`;
2454
2505
  }
2455
2506
  case "in": {
2456
2507
  const type = column(node.field)?.type;
2508
+ if (type === "json") {
2509
+ return node.values.every(readsBackAsWritten) ? jsonColumnIn(node.field, node.values, negated, params) : null;
2510
+ }
2457
2511
  if (!type || node.values.some(
2458
2512
  (v) => v !== null && v !== void 0 && typeof v !== VALUE_KIND[type]
2459
2513
  )) {
@@ -2473,6 +2527,7 @@ function compileNode(node, negated, columns, params) {
2473
2527
  case "includes": {
2474
2528
  const type = column(node.field)?.type;
2475
2529
  if (type === "json") {
2530
+ const col = node.field.sql;
2476
2531
  let element;
2477
2532
  if (typeof node.needle === "boolean") {
2478
2533
  element = `type = '${node.needle}'`;
@@ -2480,7 +2535,9 @@ function compileNode(node, negated, columns, params) {
2480
2535
  params.push(node.needle);
2481
2536
  element = `type IN ${JSON_TYPES[typeof node.needle]} AND value = ?`;
2482
2537
  }
2483
- return `(json_type(${node.field.sql}) = 'array' AND ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${node.field.sql}) WHERE ${element}))`;
2538
+ const substring = `${negated ? "=" : ">"} 0`;
2539
+ params.push(String(node.needle), String(node.needle));
2540
+ return `(CASE WHEN json_valid(${col}) THEN CASE json_type(${col}) WHEN 'array' THEN ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${col}) WHERE ${element}) WHEN 'text' THEN instr(${col} ->> '$', ?) ${substring} ELSE 0 END ELSE instr(${col}, ?) ${substring} END)`;
2484
2541
  }
2485
2542
  if ((type === "text" || type === "user") && typeof node.needle === "string") {
2486
2543
  params.push(node.needle);
@@ -9275,6 +9332,7 @@ function summarizeInput(input) {
9275
9332
  async function cmdAsk(question, options) {
9276
9333
  try {
9277
9334
  let lineBuffer = "";
9335
+ const startedAt = Date.now();
9278
9336
  const result = await runAsk(question, options, (event) => {
9279
9337
  switch (event.type) {
9280
9338
  case "text":
@@ -9304,6 +9362,12 @@ async function cmdAsk(question, options) {
9304
9362
  if (lineBuffer) {
9305
9363
  process.stderr.write(lineBuffer);
9306
9364
  }
9365
+ logUsage({
9366
+ action: "ask",
9367
+ billingCost: result.billingCost,
9368
+ billingEvents: result.billingEvents,
9369
+ startedAt
9370
+ });
9307
9371
  if (options.json) {
9308
9372
  process.stdout.write(
9309
9373
  JSON.stringify({
@@ -9328,6 +9392,7 @@ var init_ask = __esm({
9328
9392
  async "src/ask/index.ts"() {
9329
9393
  "use strict";
9330
9394
  await init_client();
9395
+ init_usageLog();
9331
9396
  init_context2();
9332
9397
  init_sse();
9333
9398
  init_tools();
@@ -9402,7 +9467,7 @@ async function startMcpServer(options) {
9402
9467
  capabilities: { tools: {} },
9403
9468
  serverInfo: {
9404
9469
  name: "mindstudio-agent",
9405
- version: "0.1.163"
9470
+ version: "0.1.165"
9406
9471
  },
9407
9472
  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.'
9408
9473
  });
@@ -9853,6 +9918,7 @@ var init_mcp = __esm({
9853
9918
  });
9854
9919
 
9855
9920
  // src/cli.ts
9921
+ init_usageLog();
9856
9922
  import { parseArgs } from "util";
9857
9923
  import { execSync } from "child_process";
9858
9924
  import { readFileSync as readFileSync3 } from "fs";
@@ -10172,11 +10238,18 @@ async function cmdExec(method, input, options) {
10172
10238
  `);
10173
10239
  };
10174
10240
  }
10241
+ const startedAt = Date.now();
10175
10242
  const result = await agent.executeStep(meta.stepType, input, {
10176
10243
  appId: options.appId,
10177
10244
  threadId: options.threadId,
10178
10245
  onLog
10179
10246
  });
10247
+ logUsage({
10248
+ action: camelToKebab(method),
10249
+ billingCost: result.$billingCost,
10250
+ billingEvents: result.$billingEvents,
10251
+ startedAt
10252
+ });
10180
10253
  if (options.outputKey) {
10181
10254
  const val = result[options.outputKey];
10182
10255
  if (typeof val === "string") {
@@ -10352,10 +10425,18 @@ Example: mindstudio batch '[{"stepType":"generateImage","step":{"prompt":"a cat"
10352
10425
  return { stepType: item.stepType, step: item.step };
10353
10426
  });
10354
10427
  const agent = await createAgent(options);
10428
+ const startedAt = Date.now();
10355
10429
  const result = await agent.executeStepBatch(steps, {
10356
10430
  appId: options.appId,
10357
10431
  threadId: options.threadId
10358
10432
  });
10433
+ for (const step of result.results) {
10434
+ logUsage({
10435
+ action: `batch:${step.stepType}`,
10436
+ billingCost: step.billingCost,
10437
+ startedAt
10438
+ });
10439
+ }
10359
10440
  if (options.noMeta) {
10360
10441
  process.stdout.write(JSON.stringify(result.results, null, 2) + "\n");
10361
10442
  } else {
@@ -10426,7 +10507,7 @@ function isNewerVersion(current, latest) {
10426
10507
  return false;
10427
10508
  }
10428
10509
  async function checkForUpdate() {
10429
- const currentVersion = "0.1.163";
10510
+ const currentVersion = "0.1.165";
10430
10511
  if (!currentVersion || process.env.MINDSTUDIO_NO_UPDATE_CHECK) {
10431
10512
  return null;
10432
10513
  }
@@ -10461,7 +10542,7 @@ async function checkForUpdate() {
10461
10542
  }
10462
10543
  }
10463
10544
  function printUpdateNotice(latestVersion) {
10464
- const currentVersion = "0.1.163";
10545
+ const currentVersion = "0.1.165";
10465
10546
  process.stderr.write(
10466
10547
  `
10467
10548
  ${ansi2.cyanBright("Update available")} ${ansi2.gray(currentVersion + " \u2192")} ${ansi2.cyanBold(latestVersion)}
@@ -10474,7 +10555,7 @@ function isStandaloneBinary() {
10474
10555
  return !argv1.includes("node_modules");
10475
10556
  }
10476
10557
  async function cmdUpdate() {
10477
- const currentVersion = "0.1.163";
10558
+ const currentVersion = "0.1.165";
10478
10559
  process.stderr.write(
10479
10560
  ` ${ansi2.gray("Current version:")} ${currentVersion}
10480
10561
  `
@@ -10615,7 +10696,7 @@ async function cmdLogin(options) {
10615
10696
  process.stderr.write("\n");
10616
10697
  printLogo();
10617
10698
  process.stderr.write("\n");
10618
- const ver = "0.1.163";
10699
+ const ver = "0.1.165";
10619
10700
  process.stderr.write(
10620
10701
  ` ${ansi2.bold("MindStudio Agent")} ${ver ? " " + ansi2.gray("v" + ver) : ""}
10621
10702
  `
@@ -10943,7 +11024,7 @@ async function main() {
10943
11024
  const updatePromise = command !== "mcp" && command !== "login" && command !== "update" ? checkForUpdate() : Promise.resolve(null);
10944
11025
  try {
10945
11026
  if (command === "version" || command === "-v") {
10946
- process.stdout.write("0.1.163\n");
11027
+ process.stdout.write("0.1.165\n");
10947
11028
  return;
10948
11029
  }
10949
11030
  if (command === "login") {
package/dist/index.js CHANGED
@@ -1594,6 +1594,25 @@ function nestedComparison(node, negated, params) {
1594
1594
  function withNulls(core, field, nullsMatch) {
1595
1595
  return nullsMatch ? `(${core} OR ${field.sql} IS NULL)` : core;
1596
1596
  }
1597
+ function readsBackAsWritten(value) {
1598
+ if (typeof value !== "string") {
1599
+ return false;
1600
+ }
1601
+ try {
1602
+ JSON.parse(value);
1603
+ return false;
1604
+ } catch {
1605
+ return true;
1606
+ }
1607
+ }
1608
+ function jsonColumnIn(field, values, negated, params) {
1609
+ params.push(JSON.stringify(values.flatMap((v) => [v, JSON.stringify(v)])));
1610
+ return withNulls(
1611
+ `${field.sql} ${negated ? "NOT IN" : "IN"} (SELECT value FROM json_each(?))`,
1612
+ field,
1613
+ negated
1614
+ );
1615
+ }
1597
1616
  function compileNode(node, negated, columns, params) {
1598
1617
  const column = (field) => field.nested ? void 0 : columns.find((c) => c.name === field.column);
1599
1618
  const stored = (field, value) => field.nested ? serializeColumnParam(value, "", []) : serializeColumnParam(value, field.column, columns);
@@ -1611,6 +1630,10 @@ function compileNode(node, negated, columns, params) {
1611
1630
  return nestedComparison(node, negated, params);
1612
1631
  }
1613
1632
  const type = column(node.field)?.type;
1633
+ if (type === "json") {
1634
+ const strictEquals = node.op === "===" ? !negated : node.op === "!==" ? negated : null;
1635
+ return strictEquals !== null && readsBackAsWritten(node.value) ? jsonColumnIn(node.field, [node.value], !strictEquals, params) : null;
1636
+ }
1614
1637
  if (!type || typeof node.value !== VALUE_KIND[type]) {
1615
1638
  return null;
1616
1639
  }
@@ -1631,10 +1654,16 @@ function compileNode(node, negated, columns, params) {
1631
1654
  return null;
1632
1655
  }
1633
1656
  const isNull = negated ? !node.isNull : node.isNull;
1657
+ if (column(node.field)?.type === "json") {
1658
+ return isNull ? `(${node.field.sql} IS NULL OR ${node.field.sql} = 'null')` : `(${node.field.sql} IS NOT NULL AND ${node.field.sql} <> 'null')`;
1659
+ }
1634
1660
  return `${node.field.sql} ${isNull ? "IS NULL" : "IS NOT NULL"}`;
1635
1661
  }
1636
1662
  case "in": {
1637
1663
  const type = column(node.field)?.type;
1664
+ if (type === "json") {
1665
+ return node.values.every(readsBackAsWritten) ? jsonColumnIn(node.field, node.values, negated, params) : null;
1666
+ }
1638
1667
  if (!type || node.values.some(
1639
1668
  (v) => v !== null && v !== void 0 && typeof v !== VALUE_KIND[type]
1640
1669
  )) {
@@ -1654,6 +1683,7 @@ function compileNode(node, negated, columns, params) {
1654
1683
  case "includes": {
1655
1684
  const type = column(node.field)?.type;
1656
1685
  if (type === "json") {
1686
+ const col = node.field.sql;
1657
1687
  let element;
1658
1688
  if (typeof node.needle === "boolean") {
1659
1689
  element = `type = '${node.needle}'`;
@@ -1661,7 +1691,9 @@ function compileNode(node, negated, columns, params) {
1661
1691
  params.push(node.needle);
1662
1692
  element = `type IN ${JSON_TYPES[typeof node.needle]} AND value = ?`;
1663
1693
  }
1664
- return `(json_type(${node.field.sql}) = 'array' AND ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${node.field.sql}) WHERE ${element}))`;
1694
+ const substring = `${negated ? "=" : ">"} 0`;
1695
+ params.push(String(node.needle), String(node.needle));
1696
+ return `(CASE WHEN json_valid(${col}) THEN CASE json_type(${col}) WHEN 'array' THEN ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${col}) WHERE ${element}) WHEN 'text' THEN instr(${col} ->> '$', ?) ${substring} ELSE 0 END ELSE instr(${col}, ?) ${substring} END)`;
1665
1697
  }
1666
1698
  if ((type === "text" || type === "user") && typeof node.needle === "string") {
1667
1699
  params.push(node.needle);