@mindstudio-ai/agent 0.1.163 → 0.1.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,47 @@
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.164
8
+
9
+ ### New
10
+
11
+ - The package carries this changelog, `CHANGELOG.md`, from this version on. Its entries start at 0.1.163.
12
+
13
+ ### Fixed
14
+
15
+ - 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.
16
+ - `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.
17
+
18
+ ## 0.1.163
19
+
20
+ Published by release v1.0.2832.
21
+
22
+ ### Changed behavior
23
+
24
+ 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.
25
+
26
+ - `!==` 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 &&`.
27
+ - `<`, `<=`, `>` 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.
28
+ - `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`).
29
+ - `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'`.
30
+ - 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.
31
+ - 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.
32
+ - 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.
33
+ - 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.
34
+
35
+ ### New
36
+
37
+ - 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.
38
+ - Comparisons compile either way round: `5000 < o.amount`.
39
+ - `.select(...columns)` on a query or table returns only those columns (`SELECT id, name`). Filters and sorts still see the whole row.
40
+ - 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.
41
+ - 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.
42
+
43
+ ### Fixed
44
+
45
+ - 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.
46
+ - `skip()` without `take()` failed with a SQL error.
47
+ - `[null, 'a'].includes(o.x)` didn't match rows where the field is empty.
package/dist/cli.js CHANGED
@@ -2413,6 +2413,25 @@ function nestedComparison(node, negated, params) {
2413
2413
  function withNulls(core, field, nullsMatch) {
2414
2414
  return nullsMatch ? `(${core} OR ${field.sql} IS NULL)` : core;
2415
2415
  }
2416
+ function readsBackAsWritten(value) {
2417
+ if (typeof value !== "string") {
2418
+ return false;
2419
+ }
2420
+ try {
2421
+ JSON.parse(value);
2422
+ return false;
2423
+ } catch {
2424
+ return true;
2425
+ }
2426
+ }
2427
+ function jsonColumnIn(field, values, negated, params) {
2428
+ params.push(JSON.stringify(values.flatMap((v) => [v, JSON.stringify(v)])));
2429
+ return withNulls(
2430
+ `${field.sql} ${negated ? "NOT IN" : "IN"} (SELECT value FROM json_each(?))`,
2431
+ field,
2432
+ negated
2433
+ );
2434
+ }
2416
2435
  function compileNode(node, negated, columns, params) {
2417
2436
  const column = (field) => field.nested ? void 0 : columns.find((c) => c.name === field.column);
2418
2437
  const stored = (field, value) => field.nested ? serializeColumnParam(value, "", []) : serializeColumnParam(value, field.column, columns);
@@ -2430,6 +2449,10 @@ function compileNode(node, negated, columns, params) {
2430
2449
  return nestedComparison(node, negated, params);
2431
2450
  }
2432
2451
  const type = column(node.field)?.type;
2452
+ if (type === "json") {
2453
+ const strictEquals = node.op === "===" ? !negated : node.op === "!==" ? negated : null;
2454
+ return strictEquals !== null && readsBackAsWritten(node.value) ? jsonColumnIn(node.field, [node.value], !strictEquals, params) : null;
2455
+ }
2433
2456
  if (!type || typeof node.value !== VALUE_KIND[type]) {
2434
2457
  return null;
2435
2458
  }
@@ -2450,10 +2473,16 @@ function compileNode(node, negated, columns, params) {
2450
2473
  return null;
2451
2474
  }
2452
2475
  const isNull = negated ? !node.isNull : node.isNull;
2476
+ if (column(node.field)?.type === "json") {
2477
+ return isNull ? `(${node.field.sql} IS NULL OR ${node.field.sql} = 'null')` : `(${node.field.sql} IS NOT NULL AND ${node.field.sql} <> 'null')`;
2478
+ }
2453
2479
  return `${node.field.sql} ${isNull ? "IS NULL" : "IS NOT NULL"}`;
2454
2480
  }
2455
2481
  case "in": {
2456
2482
  const type = column(node.field)?.type;
2483
+ if (type === "json") {
2484
+ return node.values.every(readsBackAsWritten) ? jsonColumnIn(node.field, node.values, negated, params) : null;
2485
+ }
2457
2486
  if (!type || node.values.some(
2458
2487
  (v) => v !== null && v !== void 0 && typeof v !== VALUE_KIND[type]
2459
2488
  )) {
@@ -2473,6 +2502,7 @@ function compileNode(node, negated, columns, params) {
2473
2502
  case "includes": {
2474
2503
  const type = column(node.field)?.type;
2475
2504
  if (type === "json") {
2505
+ const col = node.field.sql;
2476
2506
  let element;
2477
2507
  if (typeof node.needle === "boolean") {
2478
2508
  element = `type = '${node.needle}'`;
@@ -2480,7 +2510,9 @@ function compileNode(node, negated, columns, params) {
2480
2510
  params.push(node.needle);
2481
2511
  element = `type IN ${JSON_TYPES[typeof node.needle]} AND value = ?`;
2482
2512
  }
2483
- return `(json_type(${node.field.sql}) = 'array' AND ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${node.field.sql}) WHERE ${element}))`;
2513
+ const substring = `${negated ? "=" : ">"} 0`;
2514
+ params.push(String(node.needle), String(node.needle));
2515
+ return `(CASE WHEN json_valid(${col}) THEN CASE json_type(${col}) WHEN 'array' THEN ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${col}) WHERE ${element}) WHEN 'text' THEN instr(${col} ->> '$', ?) ${substring} ELSE 0 END ELSE instr(${col}, ?) ${substring} END)`;
2484
2516
  }
2485
2517
  if ((type === "text" || type === "user") && typeof node.needle === "string") {
2486
2518
  params.push(node.needle);
@@ -9402,7 +9434,7 @@ async function startMcpServer(options) {
9402
9434
  capabilities: { tools: {} },
9403
9435
  serverInfo: {
9404
9436
  name: "mindstudio-agent",
9405
- version: "0.1.163"
9437
+ version: "0.1.164"
9406
9438
  },
9407
9439
  instructions: 'Welcome to MindStudio \u2014 a platform with 200+ AI models, 850+ third-party integrations, and pre-built agents.\n\nGetting started:\n1. Call `ask` with any question about the SDK \u2014 it knows every action, model, and connector and returns working code with real model IDs and config options. Examples: ask("generate an image with FLUX"), ask("what models support vision?"), ask("how do I send a Slack message?").\n2. Call `changeName` to set your display name \u2014 use your name or whatever your user calls you. This is how you\'ll appear in MindStudio request logs.\n3. If you have a profile picture, pass its public image URL to `changeProfilePicture`.\n4. For manual browsing, call `listActions` to discover all available actions.\n\nThen use the tools to generate text, images, video, audio, search the web, work with data sources, run agents, and more.\n\nImportant:\n- AI-powered actions (text generation, image generation, video, audio, etc.) cost money. Before running these, call `estimateActionCost` and confirm with the user before proceeding \u2014 unless they\'ve explicitly told you to go ahead.\n- Not all agents from `listAgents` are configured for API use. Do not try to run an agent just because it appears in the list \u2014 it will likely fail. Only run agents the user specifically asks you to run.'
9408
9440
  });
@@ -10426,7 +10458,7 @@ function isNewerVersion(current, latest) {
10426
10458
  return false;
10427
10459
  }
10428
10460
  async function checkForUpdate() {
10429
- const currentVersion = "0.1.163";
10461
+ const currentVersion = "0.1.164";
10430
10462
  if (!currentVersion || process.env.MINDSTUDIO_NO_UPDATE_CHECK) {
10431
10463
  return null;
10432
10464
  }
@@ -10461,7 +10493,7 @@ async function checkForUpdate() {
10461
10493
  }
10462
10494
  }
10463
10495
  function printUpdateNotice(latestVersion) {
10464
- const currentVersion = "0.1.163";
10496
+ const currentVersion = "0.1.164";
10465
10497
  process.stderr.write(
10466
10498
  `
10467
10499
  ${ansi2.cyanBright("Update available")} ${ansi2.gray(currentVersion + " \u2192")} ${ansi2.cyanBold(latestVersion)}
@@ -10474,7 +10506,7 @@ function isStandaloneBinary() {
10474
10506
  return !argv1.includes("node_modules");
10475
10507
  }
10476
10508
  async function cmdUpdate() {
10477
- const currentVersion = "0.1.163";
10509
+ const currentVersion = "0.1.164";
10478
10510
  process.stderr.write(
10479
10511
  ` ${ansi2.gray("Current version:")} ${currentVersion}
10480
10512
  `
@@ -10615,7 +10647,7 @@ async function cmdLogin(options) {
10615
10647
  process.stderr.write("\n");
10616
10648
  printLogo();
10617
10649
  process.stderr.write("\n");
10618
- const ver = "0.1.163";
10650
+ const ver = "0.1.164";
10619
10651
  process.stderr.write(
10620
10652
  ` ${ansi2.bold("MindStudio Agent")} ${ver ? " " + ansi2.gray("v" + ver) : ""}
10621
10653
  `
@@ -10943,7 +10975,7 @@ async function main() {
10943
10975
  const updatePromise = command !== "mcp" && command !== "login" && command !== "update" ? checkForUpdate() : Promise.resolve(null);
10944
10976
  try {
10945
10977
  if (command === "version" || command === "-v") {
10946
- process.stdout.write("0.1.163\n");
10978
+ process.stdout.write("0.1.164\n");
10947
10979
  return;
10948
10980
  }
10949
10981
  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);