@lotics/cli 0.313.1 → 0.313.2

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/src/cli.js CHANGED
@@ -26416,29 +26416,16 @@ var tableRecordSelectMemberFieldFilterSchema = zod_default.object({
26416
26416
  ]),
26417
26417
  value: zod_default.array(zod_default.string()).nullable()
26418
26418
  });
26419
+ var LINK_MEMBERSHIP = ["has_any_of", "has_none_of", "has_all_of"];
26420
+ var LINK_TEXT = ["contains", "not_contains", "starts_with", "ends_with"];
26419
26421
  var tableRecordSelectRecordLinkFieldFilterSchema = zod_default.object({
26420
26422
  type: zod_default.literal("select_record_link"),
26421
26423
  field_key: zod_default.string(),
26422
- operator: zod_default.enum([
26423
- // Membership operators — match linked record IDs. value: string[].
26424
- "has_any_of",
26425
- "has_none_of",
26426
- "has_all_of",
26427
- // Text operators — match the cached display text. value: string.
26428
- "contains",
26429
- "not_contains",
26430
- "starts_with",
26431
- "ends_with",
26432
- "is_empty",
26433
- "is_not_empty"
26434
- ]),
26424
+ operator: zod_default.enum([...LINK_MEMBERSHIP, ...LINK_TEXT, "is_empty", "is_not_empty"]),
26435
26425
  value: zod_default.union([zod_default.string(), zod_default.array(zod_default.string())]).nullable()
26436
26426
  });
26437
- var SELECT_RECORD_LINK_MEMBERSHIP_OPERATORS = /* @__PURE__ */ new Set([
26438
- "has_any_of",
26439
- "has_none_of",
26440
- "has_all_of"
26441
- ]);
26427
+ var SELECT_RECORD_LINK_MEMBERSHIP_OPERATORS = new Set(LINK_MEMBERSHIP);
26428
+ var SELECT_RECORD_LINK_TEXT_OPERATORS = new Set(LINK_TEXT);
26442
26429
  var tableRecordFilesFieldFilterSchema = zod_default.object({
26443
26430
  type: zod_default.literal("files"),
26444
26431
  field_key: zod_default.string(),
@@ -46897,8 +46884,8 @@ async function saveSignedLinks(links, dir) {
46897
46884
  }
46898
46885
 
46899
46886
  // src/version.ts
46900
- var VERSION = "0.313.1";
46901
- var APP_SDK_VERSION = "0.123.1";
46887
+ var VERSION = "0.313.2";
46888
+ var APP_SDK_VERSION = "0.123.2";
46902
46889
 
46903
46890
  // src/timezone.ts
46904
46891
  function machineTimezone() {
@@ -46938,7 +46925,7 @@ var cli_reference_default = "# @lotics/cli \u2014 CLI Command Reference\n\nPer-c
46938
46925
  var data_model_default = '# The data model \u2014 tables, their fields, and how they relate\n\nThe decisions here outlive any one app, and most become expensive the moment a second screen depends\non them. They stand apart from building an app on purpose: **every workspace starts with tables\nand many never get an app**, so schema design is not a chapter of app building.\n\nThe rules below share one signature. **Both sides read correctly on their own**, so nothing reports\nthe problem \u2014 no error, no empty column, no failing query. Each is found by looking for it. Then,\nunder **Fields**, what each field type takes.\n\n## One fact, one column\n\n**Read the table\'s existing fields first, and add nothing that stores a fact the table already\nstores.** A value written as text and the same value held as a `select_record_link` are one fact in\ntwo columns \u2014 a customer\'s city typed into a text box beside a link to the city record, a status\nword beside the select that decides it, a total beside the formula that computes it.\n\nTwo columns for one fact do not stay equal. Some writer sets only one of them, and nothing reports\nthe divergence: both rows still look correct on their own. A reader that then matches on the text\nhalf treats "Acme" and "Acme Ltd" as different records, so an import creates a duplicate every time\nit runs.\n\n- **Prefer the link, the select, or the formula.** Text is a RENDERING of a record; compose it when\n you read, rather than storing it a second time.\n- **Renaming, retyping or re-pointing the existing field beats adding another.** A field\'s key is\n stable, so a rename breaks nothing that addresses it by key.\n- **Superseding a field means DELETING it**, not leaving it beside its replacement with a\n description that says which one is real.\n- **Empty is not the same as redundant.** A field nothing fills may still be the only home for a\n real distinction \u2014 read what it MEANS before removing it.\n\n## One entity, one table \u2014 and the test is measurable\n\nVariation belongs in a column \u2014 a multi-select role, a kind, a stage \u2014 not in a second table. Two\ntables for one kind of thing give the same real-world entity two rows, two ids and two halves of its\nhistory, and each screen shows whichever half it happens to link to.\n\nSplit tables are often right. A supplier book beside a customer book is a normal shape, and merging\non suspicion is a large repoint bought for nothing. So do not argue it in the abstract:\n\n> **List both tables\' names and look for one that appears in both.**\n\nNone means the split is holding. One means it has broken \u2014 the usual cause is a party you begin to\ninvoice as well as buy from \u2014 and the fix is to merge before a second screen depends on the copy.\n\nWorth writing as a test rather than a note, because a note about a condition nobody re-checks goes\nstale in silence.\n\n## One vocabulary wherever values are COPIED between tables\n\nTwo `select` fields for one concept carry DIFFERENT option keys even when their labels match \u2014 keys\nare minted per field. So anything moving a value between them needs a hand-written key map.\n\nThat map is code. Put it in one named module with a test; written inline at the copy it is invisible,\nuntested, and silently wrong the first time somebody renames an option, because a rename leaves the\nkey intact and the map still compiling. Prefer a link to a shared reference table where the set is\nopen or growing; keep a map only for a small closed set.\n\nWhere one side genuinely holds MORE values than the other, that is not drift \u2014 it is the model\ntelling the truth. The wider side must **refuse** what the narrower one cannot express rather than\nquietly picking the nearest value.\n\n## A copy boundary accounts for EVERY source field\n\nEach field on the source gets a column on the destination, a deliberate drop with the reason written\ndown, or a refusal.\n\nA field with nowhere to land is data destroyed at the boundary, and it is invisible afterwards: the\ndestination is not empty and not obviously wrong \u2014 just a number that no longer agrees with where it\ncame from.\n\n## Provenance is a LINK, not a flag and not a copy\n\nA row created BY another row carries a link to it.\n\nThat link is what makes "is this the estimate or the actual", "where did this come from" and "have we\nalready imported this" answerable at all. A boolean records that something was true once; a link\nstays true, survives a rename, and lets the next write UPDATE the original instead of adding a second\nrow beside it.\n\n## Say what makes two rows the SAME row\n\nDeclare the natural key in the table\'s description.\n\nAnything that imports, reconciles or de-duplicates has to decide identity, and with no declared key\nit falls back to comparing displayed text \u2014 which is how one company arrives three times under three\nspellings. Name the key: a reference number, a tax id, a link plus a period. Then match on `rec_\u2026`\nand `opt_\u2026`, never on rendered labels.\n\n## A state\'s HISTORY is rows, not columns\n\nA `changed at` column says only how long a row has been where it is now \u2014 the next move overwrites\nit \u2014 and a date column per state holds until something re-enters a state it already left.\n\nMeasuring time-in-state or conversion needs one ROW per move: a link to the subject, the state left,\nthe state entered, when. Hold those states as the source field\'s own `opt_` keys so the log carries\nno second vocabulary, and write the row in the workflow that makes the move.\n\n## Keep derived chains shallow\n\nFormulas and rollups are computed and STORED when a row is written, and one that reads another\nrecomputes with it. A rollup over a formula over a formula is paid three times on every touch, and\nagain for every row upstream of it.\n\n**Depth costs more than row count.** This is the optimisation lever that actually exists here; row\nscanning is the platform\'s problem, chain depth is yours.\n\n## Changing a money formula on a live table\n\nFormula edits recompute every row, so the only honest proof that one changed nothing it should not is\nthe numbers themselves:\n\n1. Snapshot the affected totals to a file.\n2. Make the change.\n3. Diff. Identical is the pass.\n\nAnd when a formula gains a new field, **test that the value IS the one you want, never that it\ndiffers from it** \u2014 an empty cell reads as `""`, which differs from every option key, so the inverted\nspelling silently zeroes every row written before the field existed.\n\n## Fields\n\nWhat `create_table` (on the CLI or in chat) and `update_table` take in `add_fields`, and `update_table` in `update_fields`.\n\n### Types and formats\n\n`type` is one of `text`, `number`, `date`, `boolean`, `select`, `select_member`,\n`select_record_link`, `files`, `formula`, `rollup`, `lookup`, `autonumber`. `button` is retired: an\nexisting button field keeps running, but none is created, converted to or edited.\n\nA URL, an email, markdown, a checkbox, a datetime, money, a quantity or a percentage is a `format`\nor a `notation` on another type, never a `type`:\n\n| Wanted | Field |\n|---|---|\n| URL or external link | `{ type: "text", format: "link" }` |\n| Email or phone | `{ type: "text" }` |\n| Markdown | `{ type: "text", format: "markdown" }` |\n| Checkbox | `{ type: "boolean" }` |\n| Money | `{ type: "number", notation: { style: "currency", currency: "USD" } }` |\n| Quantity | `{ type: "number", notation: { style: "unit", unit: "kg" } }` |\n| Percentage | `{ type: "number", notation: { style: "unit", unit: "percent" } }` |\n| Datetime | `{ type: "date", format: "datetime" }` |\n| Date range | `{ type: "date", format: "date_range" }` |\n\n### Properties\n\nA type\'s properties sit **directly on the field object** \u2014 there is no `config` wrapper. A property\nmay itself hold an object (`formula`, `aggregate_option`, `filter`, `order_by`); its inner keys stay\ninside it. Each type takes only its own:\n\n- **text** \u2014 `format?` (`"text"` | `"link"` | `"markdown"`), `unique?`, `default_value?` (a string).\n- **number** \u2014 `notation?`, `default_value?` (a number). `notation` is how the figure reads, one\n of three; on `update_fields` a stated one replaces the field\'s whole.\n - `{ style: "decimal" }` \u2014 a plain number, as a field stating none reads.\n - `{ style: "currency", currency: "VND" }` \u2014 money in an ISO 4217 code.\n - `{ style: "unit", unit }` \u2014 a quantity: `"percent"` (10 reads "10%", never multiplied), a\n measured code \u2014 g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day \u2014 or a counted noun such\n as `ki\u1EC7n`. A change between two units of one dimension converts every stored figure; any other\n change relabels.\n - A currency or unit read on each row is `{ per_row: "\u0110VT" }` in its place: a single select on\n the same row, or a lookup of one through a one-link, named by name or key, whose chosen option\n is that row\'s currency or unit \u2014 every option label an ISO 4217 code, or a unit as `unit`\n takes one.\n- **date** \u2014 `format?` (`"date"` | `"datetime"` | `"date_range"` | `"datetime_range"`), `timezone?`,\n `derive_from?`, `default_value?` (a date string). `derive_from: "created_at"` stamps the row\'s\n creation once, `"updated_at"` re-stamps on every update; the field is then read-only, takes no\n `default_value`, and holds only the `date` and `datetime` formats.\n- **boolean** \u2014 `default_value?` (`true` | `false`).\n- **select** \u2014 `options` `[{ name, color?, mark? }]`, `multi?`, `default_value?`. An option stated without a `color` gets one no sibling option holds.\n- **select_member** \u2014 `multi?`, `default_value?` (member ids).\n- **select_record_link** \u2014 `table_id`, `sync_both_ways?`, `cardinality?`, `paired_field_name?`. A\n link shows the row it names by the one field its table names its rows by \u2014 the table\'s\n `title_field_key`, stated when the table is made or changed, or by `records.<entity>.title` in the\n model; a link states no display fields of its own, and a combined name is a formula field named there.\n `sync_both_ways: true` on an existing one-way link makes it two-way \u2014 never add a second link for\n that. `cardinality: "one"` holds a single row, paired or not; a paired partner of a `"one"` side\n lists that side\'s rows as its children. Two paired sides cannot both be `"one"`. `paired_field_name`\n names the back-reference the target gets.\n- **files** \u2014 no properties.\n- **autonumber** \u2014 `template` (`"INV-{YEAR}-{N:4}"`; tokens `{N}`, `{N:W}` zero-padded to W,\n `{YEAR}`, `{YEAR:2}`, `{MONTH}`, `{DAY}`), or `prefix` + `padding` (1\u201320) for `PREFIX-0001`.\n- **any type**, at create \u2014 `confirm_before_update?`: a person confirms each later edit.\n\n`default_value` fills the field on a NEW record that states no value; existing records are never\nbackfilled, and null clears it. A select\'s or a member field\'s default is an ARRAY even when the\nfield holds one (`["Unread"]`, never `"Unread"`) \u2014 option names in `add_fields`, where the keys do\nnot exist yet, and option keys (`opt_\u2026`) in `update_fields`.\n\n`default_from` (text, number, date, boolean, select, select_member, select_record_link) copies a\nvalue onto a NEW record off the row one of its links names: `{ link_field_key, field_key,\noptions? }`, the link of cardinality `"one"` and not itself copied, the source a field of the linked\ntable of this field\'s own type, date format and notation (a link one to the same table) \u2014 a formula,\na rollup, or a lookup through a one-row link (or an ordered one) read as the value it holds; several\nvalues only into a field holding several; never into a date `derive_from` stamps. It is\ncopied when the record is made and never re-read, so a later change to the source leaves the record\nalone (a line\'s price as it was when the line was booked). A value the write states wins, then the\ncopy, then `default_value`. A select source\'s `options` maps its option keys onto this field\'s\n(`update_fields`, once the keys exist); an option left unmapped is not copied. null clears it. Deleting the link a\ncopy reads through, or the field it copies off, is refused even with `force` until the copy is\ncleared; the dependency report lists the copying field as `copy_link` or `copy_source` under\n`requires_manual_action`. Any other schema change that leaves a copy unable to stand \u2014 its source\nretyped, its link made to hold many \u2014 is refused, naming the copy.\n\n### Computed fields\n\nRead-only in records. `formula` is one key holding an object; a rollup\'s and a lookup\'s properties\nare separate keys on the field \u2014 `rollup: {\u2026}` and `lookup: {\u2026}` are refused as unrecognized.\n\nformula: { expression, notation?, link? }\n\n- `{Field Name}` or `{fld_key}` names a field of the same table, stored as its key, so a rename never\n breaks the formula. A reference to no field, or a call to something that is no helper, is refused.\n- A select reads as an ARRAY of option keys: `{Status}[0]` for a single select,\n `includes({Tags}, "opt_\u2026")` for a multi.\n- The operators, the helpers and how an empty cell reads are the `model` reference, section `formula` \u2014 a model\n names fields by alias where a table tool names them by name or key; the language is the same.\n- `notation` is how a number result reads, as a number field\'s; `link: true` says the result is a\n URL, drawn as a link that opens.\n- On `update_fields` a key left out keeps its stored value, and a stated `notation` replaces the\n stored one whole. `get_table` marks a formula that is null when every field it reads is empty.\n\nrollup \u2014 `source_field_key`, `aggregate_option: { field_key?, operation }`, `filter?`\n\n- `source_field_key` is a `select_record_link` of this table; `aggregate_option.field_key` is a\n field of the linked table, required by every operation but `count`, which counts linked records.\n- Operations by the aggregated field\'s type \u2014 none: `count`; any: `empty`, `filled`,\n `percent_empty`, `percent_filled`, `unique`, `percent_unique`; number: `sum`, `avg`, `median`,\n `min`, `max`, `range`; date: `earliest`, `latest`, `date_range`.\n- The cell\'s type comes from the OPERATION: `earliest` and `latest` hold a date, every other\n operation a number (`date_range` counts days). A "most recent linked date" is `latest` \u2014 `min` and\n `max` are numeric and refuse a date. A rollup takes no notation of its own: `sum` over money\n reads in the aggregated field\'s currency, over a weight in its unit, a count as a plain number\n and a `percent_*` in the unit `percent`.\n- Over a figure read in each row\'s own unit or currency (a notation\'s `per_row`), `sum`,\n `avg`, `median`, `min`, `max` and `range` hold only where that select is a lookup, through the\n link paired with `source_field_key`, of a single select on this table \u2014 the total reads in this\n row\'s option of it. A lookup of such a field has no unit.\n- `filter` aggregates only the linked records it matches: a full group over the LINKED table\'s\n fields \u2014 `{ node_type: "group", logic, children: [{ node_type: "condition", type, field_key,\n operator, value }] }`, a select condition using `has_any_of` with an ARRAY value.\n\nlookup \u2014 `source_field_key`, `lookup_field_key`, `order_by?`\n\n- `lookup_field_key` is a field of the linked table. Without `order_by` the cell holds every linked\n record\'s value; with `order_by: { field_key, direction }` (`desc` latest, `asc` earliest) it holds\n the picked field of the single extreme record \u2014 a "latest linked X" that maintains itself.\n Ordered lookups sharing one `order_by` resolve to the SAME record.\n\n### Examples\n\n```\n{ name: "Gi\xE1 b\xE1n", type: "number", notation: { style: "currency", currency: "VND" } }\n{ name: "Tr\u1ECDng l\u01B0\u1EE3ng", type: "number", notation: { style: "unit", unit: "kg" } }\n{ name: "S\u1ED1 l\u01B0\u1EE3ng", type: "number", notation: { style: "unit", unit: { per_row: "\u0110VT" } } }\n{ name: "Chi\u1EBFt kh\u1EA5u", type: "number", notation: { style: "unit", unit: "percent" } }\n{ name: "Tr\u1EA1ng th\xE1i", type: "select", options: [{ name: "M\u1EDBi" }, { name: "Xong" }] }\n{ name: "M\xE3 \u0111\u01A1n", type: "autonumber", template: "SR-{YEAR}-{N:4}" }\n{ name: "Kh\xE1ch h\xE0ng", type: "select_record_link", table_id: "tbl_x", sync_both_ways: true }\n{ name: "Total", type: "formula", formula: { expression: "{Price} * {Qty}", notation: { style: "currency", currency: "VND" } } }\n{ name: "SL \u0111\xE3 giao", type: "rollup", source_field_key: "Giao h\xE0ng", aggregate_option: { operation: "sum", field_key: "S\u1ED1 l\u01B0\u1EE3ng" } }\n{ name: "Customer Name", type: "lookup", source_field_key: "Customer", lookup_field_key: "Name" }\n{ name: "Latest note", type: "lookup", source_field_key: "Calls", lookup_field_key: "Note", order_by: { field_key: "At", direction: "desc" } }\n```\n\n### Changing a field\n\nA field added by `update_table` shows in a view only when `add_to_views` names it, at the view\'s far\nright; on the CLI or in chat, `update_view` with `move_field` places it beside the columns it belongs with.\n\nAn `update_fields` entry is `{ field_key, name?, description?, convert_to?, \u2026 }` with the same flat\nproperties as `add_fields`, plus what only an update does:\n\n- A select\'s options change through `add_options` `[{ name, color?, mark? }]`, `update_options`\n `[{ key, name?, color?, mark? }]`, `remove_option_keys` and `reorder_options` (every existing\n option once, in order; options added in the same call follow). An option is named by its `opt_\u2026`\n key (its name also resolves); a rename keeps every record\'s value. `mark` is the option\'s brand\n (`{ kind: "brand", name: "tiktok" }`) or kit glyph (`{ kind: "icon", name: "wrench" }`), drawn in\n place of its colour dot \u2014 every option of a field has one or none does, so marking sets them all\n in one call and `mark: null` on each clears them. `distinct_option_colors: true` re-colours a\n select so every option holds its own: the first option of each colour keeps it, the rest take\n colours no option holds.\n- A link\'s `sync_both_ways: false` disconnects the pair; a new `table_id` re-points it and clears its\n record data; `cardinality` is `"one"` for the child side of a parent-child pair, `"many"` for peers.\n';
46939
46926
 
46940
46927
  // docs/filters.md
46941
- var filters_default = '# Filters \u2014 choosing records by their fields\n\nOne grammar for every tool that takes a filter: `query_records`, `aggregate_records`, `update_records`\nand `delete_records` by filter, a view\'s filter, a rollup\'s filter, and the filters a caller passes\nto an app\'s query.\n\n## The shape\n\nA filter is a single condition, or a group of conditions.\n\n```\n{ "node_type": "condition", "field_key": "Status", "operator": "has_any_of", "value": ["Done"] }\n{ "node_type": "group", "logic": "and" | "or", "children": [ \u2026conditions or groups ] }\n```\n\nGroups nest up to 4 levels. AND(OR(status = A, status = B), age > 40):\n\n```\n{ "node_type": "group", "logic": "and", "children": [\n { "node_type": "group", "logic": "or", "children": [cond1, cond2] },\n cond3\n] }\n```\n\nEvery `field_key` resolves against the table being filtered \u2014 read each table\'s fields (`get_table`)\nand carry no name across tables: sibling tables holding the same concept routinely name it\ndifferently. A field is named by its `fld_\u2026` key or by its name (`"Status"`).\n\n## Text\n\n| Operators | Value |\n|---|---|\n| `equals`, `not_equals`, `contains`, `does_not_contain`, `starts_with`, `ends_with` | a string |\n| `is_any_of`, `is_none_of` | a string array \u2014 exact, case-insensitive |\n| `contains_any_of` | a string array \u2014 the cell contains any of them, case- and accent-insensitive |\n| `is_empty`, `is_not_empty` | none |\n\n```\n{ "node_type": "condition", "field_key": "Title", "operator": "is_any_of", "value": ["hello", "world"] }\n```\n\nA text value is a string, never an array; `is_any_of` matches several. Its array has no length cap \u2014\npass the whole list in one condition rather than splitting it across calls.\n\n## Number\n\n| Operators | Value |\n|---|---|\n| `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to` | a number |\n| `is_empty`, `is_not_empty` | none |\n\nA number whose `notation` reads its unit or currency on each row (`{ "per_row": <select> }`) is\nread in each row\'s own, so a comparison on it states `unit_option`: the option of that select its\nvalue is in.\nRows holding another option are out; measured units of one dimension convert.\n\n```\n{ "node_type": "condition", "field_key": "Amount", "operator": "greater_than", "value": 100, "unit_option": "USD" }\n```\n\n## Boolean\n\n`equals` with `true` or `false`.\n\n## Select\n\n| Operators | Value |\n|---|---|\n| `has_any_of` (any of them \u2014 "A or B"), `has_none_of`, `has_all_of` (a multi-select only) | an ARRAY of option names, even for one |\n| `is_empty`, `is_not_empty` | none |\n\n```\n{ "node_type": "condition", "field_key": "Status", "operator": "has_any_of", "value": ["Done", "In Progress"] }\n```\n\n## Member\n\n| Operators | Value |\n|---|---|\n| `has_any_of`, `has_none_of`, `has_all_of` | an array of member ids |\n| `is_current_member`, `is_not_current_member`, `is_empty`, `is_not_empty` | none |\n\n## Date\n\n| Operators | Value |\n|---|---|\n| `before`, `after`, `on_or_before`, `on_or_after`, `on`, `starts_before`, `starts_after`, `ends_before`, `ends_after` | a point |\n| `between`, `overlaps`, `contains`, `within` | `{ start: point \\| null, end: point \\| null }` |\n| `time_of_day` | `{ start_time: "HH:mm" \\| null, end_time: "HH:mm" \\| null }` |\n| `duration_equals`, `duration_greater_than`, `duration_less_than` | `{ amount, unit: "minutes" \\| "hours" \\| "days" }` |\n| `is_empty`, `is_not_empty` | none |\n\nA point is one of:\n\n```\n{ "type": "exact", "date": "2025-03-14", "time": "09:00" } time optional\n{ "type": "relative", "offset": -7, "unit": "minutes" | "hours" | "days" | "weeks" | "months" | "years" }\n{ "type": "period", "period": "day" | "week" | "month" | "quarter" | "year", "boundary": "start" | "end", "offset": 0 }\n```\n\n## Link\n\n| Operators | Value |\n|---|---|\n| `has_any_of` (linked to any \u2014 filters by record identity), `has_none_of`, `has_all_of` (a multi-link only) | an array of record ids (`rec_\u2026`) |\n| `contains`, `not_contains`, `starts_with`, `ends_with` | a string, matched against the linked record\'s display text |\n| `is_empty`, `is_not_empty` | none |\n\n```\n{ "node_type": "condition", "field_key": "Customer", "operator": "has_any_of", "value": ["rec_abc123"] }\n```\n\n## Sort\n\nA view\'s sort is a list, applied in order: `[{ "field_key": "Due", "order": "asc" | "desc" }]`.\n';
46928
+ var filters_default = '# Filters \u2014 choosing records by their fields\n\nOne grammar for every tool that takes a filter: `query_records`, `aggregate_records`, `update_records`\nand `delete_records` by filter, a view\'s filter, a rollup\'s filter, and the filters a caller passes\nto an app\'s query.\n\n## The shape\n\nA filter is a single condition, or a group of conditions.\n\n```\n{ "node_type": "condition", "field_key": "Status", "operator": "has_any_of", "value": ["Done"] }\n{ "node_type": "group", "logic": "and" | "or", "children": [ \u2026conditions or groups ] }\n```\n\nGroups nest up to 4 levels. AND(OR(status = A, status = B), age > 40):\n\n```\n{ "node_type": "group", "logic": "and", "children": [\n { "node_type": "group", "logic": "or", "children": [cond1, cond2] },\n cond3\n] }\n```\n\nEvery `field_key` resolves against the table being filtered \u2014 read each table\'s fields (`get_table`)\nand carry no name across tables: sibling tables holding the same concept routinely name it\ndifferently. A field is named by its `fld_\u2026` key or by its name (`"Status"`).\n\n## Text\n\n| Operators | Value |\n|---|---|\n| `equals`, `not_equals`, `contains`, `does_not_contain`, `starts_with`, `ends_with` | a string |\n| `is_any_of`, `is_none_of` | a string array \u2014 exact, case-insensitive |\n| `contains_any_of` | a string array \u2014 the cell contains any of them, case- and accent-insensitive |\n| `is_empty`, `is_not_empty` | none |\n\n```\n{ "node_type": "condition", "field_key": "Title", "operator": "is_any_of", "value": ["hello", "world"] }\n```\n\nA text value is a string, never an array; `is_any_of` matches several. Its array has no length cap \u2014\npass the whole list in one condition rather than splitting it across calls.\n\n## Number\n\n| Operators | Value |\n|---|---|\n| `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to` | a number |\n| `is_empty`, `is_not_empty` | none |\n\nA number whose `notation` reads its unit or currency on each row (`{ "per_row": <select> }`) is\nread in each row\'s own, so a comparison on it states `unit_option`: the option of that select its\nvalue is in.\nRows holding another option are out; measured units of one dimension convert.\n\n```\n{ "node_type": "condition", "field_key": "Amount", "operator": "greater_than", "value": 100, "unit_option": "USD" }\n```\n\n## Boolean\n\n`equals` with `true` or `false`.\n\n## Select\n\n| Operators | Value |\n|---|---|\n| `has_any_of` (any of them \u2014 "A or B"), `has_none_of`, `has_all_of` (a multi-select only) | an ARRAY of option names, even for one |\n| `is_empty`, `is_not_empty` | none |\n\n```\n{ "node_type": "condition", "field_key": "Status", "operator": "has_any_of", "value": ["Done", "In Progress"] }\n```\n\n## Member\n\n| Operators | Value |\n|---|---|\n| `has_any_of`, `has_none_of`, `has_all_of` | an array of member ids |\n| `is_current_member`, `is_not_current_member`, `is_empty`, `is_not_empty` | none |\n\n## Date\n\n| Operators | Value |\n|---|---|\n| `before`, `after`, `on_or_before`, `on_or_after`, `on`, `starts_before`, `starts_after`, `ends_before`, `ends_after` | a point |\n| `between`, `overlaps`, `contains`, `within` | `{ start: point \\| null, end: point \\| null }` |\n| `time_of_day` | `{ start_time: "HH:mm" \\| null, end_time: "HH:mm" \\| null }` |\n| `duration_equals`, `duration_greater_than`, `duration_less_than` | `{ amount, unit: "minutes" \\| "hours" \\| "days" }` |\n| `is_empty`, `is_not_empty` | none |\n\nA point is one of:\n\n```\n{ "type": "exact", "date": "2025-03-14", "time": "09:00" } time optional\n{ "type": "relative", "offset": -7, "unit": "minutes" | "hours" | "days" | "weeks" | "months" | "years" }\n{ "type": "period", "period": "day" | "week" | "month" | "quarter" | "year", "boundary": "start" | "end", "offset": 0 }\n```\n\n## Link\n\n| Operators | Value |\n|---|---|\n| `has_any_of` (linked to any \u2014 filters by record identity), `has_none_of`, `has_all_of` (a multi-link only) | an array of record ids (`rec_\u2026`) |\n| `contains`, `not_contains`, `starts_with`, `ends_with` \u2014 deprecated: the text a link stores changes with every rename; filter by record, and by a linked record\'s field through a lookup of it | a string, matched against the linked record\'s display text |\n| `is_empty`, `is_not_empty` | none |\n\n```\n{ "node_type": "condition", "field_key": "Customer", "operator": "has_any_of", "value": ["rec_abc123"] }\n```\n\n## Sort\n\nA view\'s sort is a list, applied in order: `[{ "field_key": "Due", "order": "asc" | "desc" }]`.\n';
46942
46929
 
46943
46930
  // docs/field_values.md
46944
46931
  var field_values_default = '# Field values \u2014 what a write takes for each field type\n\nThe value `create_records` and `update_records` take for a field.\n\n## Values by type\n\n| Type | Value |\n|---|---|\n| `text` | `"hello"` |\n| `number` | `42` |\n| `boolean` | `true` |\n| `date` | `"2025-03-14"`, or a month `"2025-03"` or a year `"2025"`, stored as written and compared as its first day |\n| `datetime` | a `date` of this format: `"2025-03-14T09:00"`, never a month or a year |\n| `date_range` | a `date` of this format: `"2025-03-01/2025-03-15"`, both halves full dates |\n| `datetime_range` | a `date` of this format: `"2025-03-01T09:00/2025-03-15T17:00"`, both halves full |\n| `select` | `["opt_\u2026"]` |\n| `select_member` | `["mbr_\u2026"]` |\n| `select_record_link` | `["rec_\u2026"]` |\n| `files` | `["fil_\u2026"]`, ids of uploaded files |\n| `formula`, `rollup`, `lookup`, `autonumber`, a `date` with `derive_from` | none \u2014 the platform writes them, and a write naming one is refused |\n\n## Lists\n\n`select`, `select_member`, `select_record_link` and `files` store an ARRAY, even where the field holds\none value.\n\n- A single-select is a ONE-element array; a bare `"opt_\u2026"` is accepted and wrapped. A single\n `select_member` takes a bare `"mbr_\u2026"` the same way.\n- `select_record_link` and `files` take an array only.\n- A `files` id the write adds must name a live file of this workspace, or the whole write is refused;\n an id that cell already holds stays, though its file was archived since.\n- Two options on a single-select, or two members on a single `select_member`, are refused.\n- `update_records`\' `add_to`, `remove_from` and `replace` take arrays of the same items: `opt_` keys\n for a select, member ids for a `select_member`, record ids for a `select_record_link`, file ids for\n `files`.\n';
package/docs/filters.md CHANGED
@@ -103,7 +103,7 @@ A point is one of:
103
103
  | Operators | Value |
104
104
  |---|---|
105
105
  | `has_any_of` (linked to any — filters by record identity), `has_none_of`, `has_all_of` (a multi-link only) | an array of record ids (`rec_…`) |
106
- | `contains`, `not_contains`, `starts_with`, `ends_with` | a string, matched against the linked record's display text |
106
+ | `contains`, `not_contains`, `starts_with`, `ends_with` — deprecated: the text a link stores changes with every rename; filter by record, and by a linked record's field through a lookup of it | a string, matched against the linked record's display text |
107
107
  | `is_empty`, `is_not_empty` | none |
108
108
 
109
109
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.313.1",
3
+ "version": "0.313.2",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {