@xanots/sdk 0.0.1

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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/dist/agent-file-refresh-7XCACU34.js +95 -0
  4. package/dist/bin.d.ts +1 -0
  5. package/dist/bin.js +184 -0
  6. package/dist/capture-HUV5BNTC.js +32 -0
  7. package/dist/chunk-26CNBZE2.js +963 -0
  8. package/dist/chunk-33ND6LKO.js +78 -0
  9. package/dist/chunk-3DUAGYGP.js +1550 -0
  10. package/dist/chunk-3IGNIP6R.js +89 -0
  11. package/dist/chunk-47WDWMBJ.js +14 -0
  12. package/dist/chunk-4BXJGVZ3.js +318 -0
  13. package/dist/chunk-4HT3BNZ7.js +675 -0
  14. package/dist/chunk-4YMD2OOZ.js +23 -0
  15. package/dist/chunk-5WGEURVI.js +156 -0
  16. package/dist/chunk-5YTBTF25.js +29 -0
  17. package/dist/chunk-6DHBYBTO.js +166 -0
  18. package/dist/chunk-76QBEIGO.js +66 -0
  19. package/dist/chunk-7DKX2SPN.js +100 -0
  20. package/dist/chunk-7REDODS2.js +468 -0
  21. package/dist/chunk-C56BC2FY.js +254 -0
  22. package/dist/chunk-DGSF2Q5H.js +60 -0
  23. package/dist/chunk-EMCAWMHC.js +3982 -0
  24. package/dist/chunk-EZG76F7R.js +159 -0
  25. package/dist/chunk-FJ6ZS5KB.js +3607 -0
  26. package/dist/chunk-GNPVYOPB.js +17 -0
  27. package/dist/chunk-HJPTWBLH.js +38 -0
  28. package/dist/chunk-IZW6MK3U.js +248 -0
  29. package/dist/chunk-LH6Z6KFV.js +247 -0
  30. package/dist/chunk-MZUI3JNX.js +93 -0
  31. package/dist/chunk-NDZFBZHC.js +167 -0
  32. package/dist/chunk-NOO35BQ2.js +37 -0
  33. package/dist/chunk-NUQCEOKA.js +13 -0
  34. package/dist/chunk-QTNO2WD6.js +56 -0
  35. package/dist/chunk-RVB7ITKN.js +49 -0
  36. package/dist/chunk-TRU5OHQ5.js +6904 -0
  37. package/dist/chunk-VKFQJEMW.js +814 -0
  38. package/dist/chunk-VTIL47DT.js +125 -0
  39. package/dist/chunk-WHOJWOSV.js +112 -0
  40. package/dist/chunk-WKP4D5MO.js +1692 -0
  41. package/dist/chunk-X4DVXBFY.js +38 -0
  42. package/dist/chunk-XQ22GLYS.js +118 -0
  43. package/dist/chunk-YDZYMZBE.js +670 -0
  44. package/dist/chunk-YGPIY3WT.js +1146 -0
  45. package/dist/chunk-YYRDLFSJ.js +602 -0
  46. package/dist/chunk-ZUTSMMAG.js +46 -0
  47. package/dist/cli.d.ts +331 -0
  48. package/dist/cli.js +33 -0
  49. package/dist/codegen-command-OOKK6AZK.js +40 -0
  50. package/dist/codegen.d.ts +114 -0
  51. package/dist/codegen.js +71 -0
  52. package/dist/completion-TIPPZN7H.js +298 -0
  53. package/dist/config-NL33PN4D.js +84 -0
  54. package/dist/deploy-command-4EQ7XX5G.js +451 -0
  55. package/dist/env-target-IFJ5QDHC.js +16 -0
  56. package/dist/ephemeral-command-3X3I4ULC.js +242 -0
  57. package/dist/index.d.ts +4477 -0
  58. package/dist/index.js +300 -0
  59. package/dist/init-command-R6H7ZXW5.js +27 -0
  60. package/dist/internal.d.ts +74 -0
  61. package/dist/internal.js +2065 -0
  62. package/dist/io-AMIKRLPC.js +11 -0
  63. package/dist/live-diff-Y3AKETHE.js +196 -0
  64. package/dist/lock-3CVKALKT.js +37 -0
  65. package/dist/lock-commands-XTR2YKLY.js +232 -0
  66. package/dist/login-command-ACJF6KWQ.js +167 -0
  67. package/dist/logout-command-MX3MJS5U.js +68 -0
  68. package/dist/loop-OM3P2Z6O.js +93 -0
  69. package/dist/marketplace-command-RATXTKXN.js +379 -0
  70. package/dist/meta-client-OW5WKWW7.js +170 -0
  71. package/dist/node.d.ts +59 -0
  72. package/dist/node.js +442 -0
  73. package/dist/profile-command-SWJ3SPKR.js +77 -0
  74. package/dist/release-command-VV5UVZKL.js +427 -0
  75. package/dist/response-CVAE2kMj.d.ts +1915 -0
  76. package/dist/routes-manifest-SP3ZXLMR.js +244 -0
  77. package/dist/runtime-V4C3AC3A.js +13 -0
  78. package/dist/sandbox-details-command-HJE5SPVG.js +18 -0
  79. package/dist/sandbox-export-command-QCJY4GMV.js +24 -0
  80. package/dist/static-host-D6KS7X45.js +138 -0
  81. package/dist/store-BQ97nKIF.d.ts +7760 -0
  82. package/dist/test-command-TSPBQHBM.js +383 -0
  83. package/dist/validate-command-4ERHJSCO.js +122 -0
  84. package/dist/verify-rollout-FAVQ6EJY.js +40 -0
  85. package/dist/workspace-K72NP7SX.js +49 -0
  86. package/dist/workspace-command-OF23DPNI.js +156 -0
  87. package/dist/workspace-export-AJMGN3CQ.js +10 -0
  88. package/llms/fields.md +104 -0
  89. package/llms/filters.md +183 -0
  90. package/llms/kinds-agent-mcp.md +11 -0
  91. package/llms/kinds-core.md +68 -0
  92. package/llms/kinds-realtime.md +76 -0
  93. package/llms/lambda.md +111 -0
  94. package/llms/legacy.md +43 -0
  95. package/llms/object-kinds.md +31 -0
  96. package/llms/statements-calls.md +23 -0
  97. package/llms/statements-catalog.md +309 -0
  98. package/llms/statements-data.md +58 -0
  99. package/llms/statements-runtime.md +48 -0
  100. package/llms/tests.md +58 -0
  101. package/llms/triggers.md +22 -0
  102. package/llms/values.md +29 -0
  103. package/llms-full.txt +1672 -0
  104. package/llms.txt +483 -0
  105. package/manifest.json +9592 -0
  106. package/package.json +110 -0
@@ -0,0 +1,156 @@
1
+ import {
2
+ fetchWorkspaceBundle,
3
+ runCodegenCommand
4
+ } from "./chunk-TRU5OHQ5.js";
5
+ import "./chunk-26CNBZE2.js";
6
+ import {
7
+ resolveOutputTarget
8
+ } from "./chunk-XQ22GLYS.js";
9
+ import "./chunk-QTNO2WD6.js";
10
+ import "./chunk-ZUTSMMAG.js";
11
+ import "./chunk-WKP4D5MO.js";
12
+ import "./chunk-VKFQJEMW.js";
13
+ import "./chunk-47WDWMBJ.js";
14
+ import "./chunk-X4DVXBFY.js";
15
+ import "./chunk-7DKX2SPN.js";
16
+ import {
17
+ printMicroserviceSection,
18
+ readMicroservices
19
+ } from "./chunk-NDZFBZHC.js";
20
+ import {
21
+ isMachineOutput,
22
+ writeJson
23
+ } from "./chunk-NUQCEOKA.js";
24
+ import {
25
+ fetchOrExplain,
26
+ httpFailure
27
+ } from "./chunk-3IGNIP6R.js";
28
+ import {
29
+ ENV_WORKSPACE,
30
+ getAccessToken
31
+ } from "./chunk-C56BC2FY.js";
32
+ import "./chunk-7REDODS2.js";
33
+ import "./chunk-MZUI3JNX.js";
34
+ import "./chunk-RVB7ITKN.js";
35
+ import "./chunk-4BXJGVZ3.js";
36
+ import "./chunk-FJ6ZS5KB.js";
37
+ import "./chunk-YGPIY3WT.js";
38
+ import {
39
+ removedSubcommand,
40
+ unknownSubcommand
41
+ } from "./chunk-IZW6MK3U.js";
42
+ import {
43
+ formatFields,
44
+ stdoutStyle,
45
+ step,
46
+ success
47
+ } from "./chunk-EZG76F7R.js";
48
+ import "./chunk-3DUAGYGP.js";
49
+ import "./chunk-YYRDLFSJ.js";
50
+ import "./chunk-EMCAWMHC.js";
51
+ import "./chunk-WHOJWOSV.js";
52
+ import "./chunk-LH6Z6KFV.js";
53
+ import "./chunk-HJPTWBLH.js";
54
+ import "./chunk-GNPVYOPB.js";
55
+ import "./chunk-4HT3BNZ7.js";
56
+
57
+ // src/emit/workspace-command.ts
58
+ import { writeFileSync } from "fs";
59
+ var TIMEOUT_MS = 3e4;
60
+ var DEFAULT_NAME = "workspace";
61
+ async function runWorkspaceCommand(args) {
62
+ switch (args.subcommand) {
63
+ case "details":
64
+ return runDetails(args);
65
+ case "export":
66
+ return runExport(args);
67
+ case "codegen":
68
+ return runCodegenCommand(args, { kind: "workspace" });
69
+ case "deploy":
70
+ throw removedSubcommand("workspace", "deploy");
71
+ default:
72
+ throw unknownSubcommand("workspace", args.subcommand);
73
+ }
74
+ }
75
+ async function fetchWorkspaces(auth) {
76
+ const url = new URL("/api:meta/workspace", auth.instance);
77
+ const res = await fetchOrExplain(
78
+ url.href,
79
+ {
80
+ headers: { accept: "application/json", Authorization: `Bearer ${auth.access_token}` },
81
+ signal: AbortSignal.timeout(TIMEOUT_MS)
82
+ },
83
+ "workspace list",
84
+ TIMEOUT_MS
85
+ );
86
+ const text = await res.text();
87
+ if (!res.ok) {
88
+ throw new Error(httpFailure("workspace list", res, text));
89
+ }
90
+ try {
91
+ const parsed = JSON.parse(text);
92
+ return Array.isArray(parsed) ? parsed : [];
93
+ } catch {
94
+ throw new Error(`workspace list: could not parse the response as JSON:
95
+ ${text.slice(0, 200)}`);
96
+ }
97
+ }
98
+ async function runDetails(args) {
99
+ const auth = await getAccessToken(args);
100
+ const workspaceId = auth.workspaceId;
101
+ const all = await fetchWorkspaces(auth);
102
+ const match = all.find((w) => w.id === workspaceId);
103
+ if (!match) {
104
+ const known = all.filter((w) => typeof w.id === "number").map((w) => ` ${String(w.id).padStart(3)} ${typeof w.name === "string" ? w.name : "(unnamed)"}`).join("\n");
105
+ const source = auth.credentialType !== "token" ? `Run \`xanots login\` again to re-pin it` : process.env[ENV_WORKSPACE] !== void 0 ? `Fix \`${ENV_WORKSPACE}\` in the environment` : `Fix \`workspace_id\` in your credential file`;
106
+ throw new Error(
107
+ `Workspace ${workspaceId} does not exist on ${new URL(auth.instance).host} (or your credential cannot see it).
108
+ ${source}.` + (known === "" ? "" : `
109
+
110
+ Workspaces you can reach:
111
+ ${known}`)
112
+ );
113
+ }
114
+ const summary = {
115
+ instance: auth.instance,
116
+ id: workspaceId,
117
+ name: typeof match.name === "string" ? match.name : void 0,
118
+ guid: typeof match.guid === "string" ? match.guid : void 0,
119
+ /** Which credential this command is acting under — the only thing that selects a workspace. */
120
+ credential: auth.credentialType
121
+ };
122
+ const microservices = await readMicroservices(auth, auth.instance, workspaceId);
123
+ if (isMachineOutput(args)) {
124
+ writeJson({ ...summary, microservices });
125
+ return;
126
+ }
127
+ const s = stdoutStyle();
128
+ const rows = [
129
+ ["instance", summary.instance],
130
+ ["workspace", `${summary.name ?? "(unnamed)"} ${s.dim(`#${summary.id}`)}`]
131
+ ];
132
+ if (summary.guid !== void 0) rows.push(["guid", summary.guid]);
133
+ rows.push([
134
+ "source",
135
+ summary.credential === "token" ? "your meta API token credential" : "your sign-in (pinned at login)"
136
+ ]);
137
+ process.stdout.write(formatFields(rows) + "\n");
138
+ printMicroserviceSection(microservices);
139
+ }
140
+ async function runExport(args) {
141
+ const auth = await getAccessToken(args);
142
+ const bundle = await fetchWorkspaceBundle(auth);
143
+ const content = JSON.stringify(bundle, null, 2);
144
+ const target = resolveOutputTarget({ path: args.path, name: args.name ?? DEFAULT_NAME, ext: "json" });
145
+ if (target.kind === "stdout") {
146
+ process.stdout.write(content + "\n");
147
+ return;
148
+ }
149
+ writeFileSync(target.path, content + "\n", "utf8");
150
+ step(`Exported workspace \u2192 ${target.path}`);
151
+ success(`Wrote ${target.path}`);
152
+ }
153
+ export {
154
+ runWorkspaceCommand
155
+ };
156
+ //# sourceMappingURL=workspace-command-OF23DPNI.js.map
@@ -0,0 +1,10 @@
1
+ import {
2
+ exportWorkspaceBundle
3
+ } from "./chunk-QTNO2WD6.js";
4
+ import "./chunk-ZUTSMMAG.js";
5
+ import "./chunk-X4DVXBFY.js";
6
+ import "./chunk-3IGNIP6R.js";
7
+ export {
8
+ exportWorkspaceBundle
9
+ };
10
+ //# sourceMappingURL=workspace-export-AJMGN3CQ.js.map
package/llms/fields.md ADDED
@@ -0,0 +1,104 @@
1
+ # Column and input types
2
+
3
+ > Read when declaring a table column (`f.*`) or a function/query input (`input.*`) — a type's options and accessor methods, and the `s.precondition` error/status contract that rides the same catalog.
4
+
5
+ Author table columns + function/API inputs with the typed catalog: `f.<type>(opts?)`
6
+ for columns, `input.<type>(opts?)` for inputs. Common opts: `required`, `nullable`,
7
+ `default`, `description`.
8
+ **`nullable` defaults PER TYPE, matching the engine's own column-creation API**: `true`
9
+ for `f.vector`, `f.uuid`, every `f.geo.*` and every file type (`f.image`/`f.video`/
10
+ `f.audio`/`f.attachment`), `false` for everything else (text, int, decimal, bool, email,
11
+ enum, json, object, password, date, tableRef). Pass `nullable` explicitly to override —
12
+ e.g. `f.geo.polygon({ nullable: false })`. This is why `f.vector(8)` deploys: the engine
13
+ turns an empty default into SQL NULL only for a nullable column, so a non-null vector
14
+ would reach PostgreSQL as `vector(8) not null default ''` and fail to create.
15
+ **`f.geo.*` values are `{ type, data }`, not GeoJSON.** The same shape goes in and comes
16
+ back: `{ type: "point", data: { lng, lat } }`, `{ type: "poly", data: [{ lng, lat }, …] }`
17
+ — `type` is the engine's abbreviation, and a polygon ring is closed for you. Raw WKT text
18
+ (`c.text("POINT(1 2)")`) is accepted on write too, but a read never returns one.
19
+ `methods` is a bind-time validator/transform pipeline whose
20
+ valid names depend on the field type (below) — pass bare names (`"trim"`), the
21
+ colon-form with args (`"min:8"`), or `{ name, arg }` for anything not listed.
22
+ `f.json({children})` declares the nested shape stored INSIDE a json column — an ARRAY of
23
+ `{name, type, methods?, children?}`, order-significant, distinct from the `FieldMap` that
24
+ `f.object` takes positionally. Omit it for an unstructured json column.
25
+ `f.enum(values)`/`f.vector(size)`/`f.object(children)`/`f.tableRef(table)` take a
26
+ positional payload before opts — and still accept the standard `FieldOptions` after it
27
+ (`f.enum([])`/`input.enum([])` are accepted, because the engine stores an enum whose
28
+ options were never filled in — that is a pulled-workspace shape, not one to author; it
29
+ brands the column `never`, which `InferRow` surfaces at read time as `undefined` — so
30
+ code written against `never` is dead in a way `undefined` is not.)
31
+ (e.g. `f.tableRef(users, { required: true })` — only `min`/`max` are listed as tableRef
32
+ methods below, but `required`/`nullable`/`description`/… apply like any field.)
33
+ An **OPTIONAL foreign key wants a `0` sentinel, not `nullable: true`.** `f.tableRef` stores
34
+ an `int`, and a null in it is unqueryable: `null` is never a legal `fieldValue`/`id`, so
35
+ `s.db.get`/`edit`/`del` on that column answer HTTP 400 `Missing param: field_value` rather
36
+ than matching nothing. Declare `f.tableRef(users, { required: true, default: 0 })` for
37
+ "not set yet" — `s.db.get({ fieldName: "driver", fieldValue: c.int(0) })` matches no row and
38
+ binds `null`, which is the answer the null was reaching for. `export()` warns on a literal
39
+ `c.null()` in that slot.
40
+ An `f.vector(size)` column is SEARCHED through `s.db.query`'s `eval` pipeline, not through
41
+ any `SearchOp`: give the table `index: [{ type: "vector", fields: [{ name: "embedding", op:
42
+ "vector_cosine_ops" }] }]`, then rank with a distance filter + a sort on its alias (see
43
+ `s.db.query` → `eval`). Without that pairing the column stores and indexes but nothing
44
+ queries it.
45
+ `{ array: true }` makes any `f.*` scalar a **list column** — `f.text({ array: true })`
46
+ surfaces as `string[]` in `InferRow<typeof table>` (the column analogue of `input.list`).
47
+ A **column `default` must stay within the BMP** — a 4-byte character (codepoint > U+FFFF,
48
+ e.g. an emoji) is mangled into invalid UTF-8 by the engine's default pipeline and is rejected
49
+ at export rather than 500ing at deploy (Postgres `22021`); BMP defaults (accents, `€`, most
50
+ CJK) are fine, or put the value on an `input.<type>({ default })`, applied at runtime bind.
51
+ `input.*` mirrors `f.*` — every column type below is
52
+ a legal input (scalars, files `input.image/video/audio/attachment`, `input.geo.*`,
53
+ `input.vector(size)`, `input.tableRef(table)`, `input.object(children)`), plus
54
+ `input.dbLink(table)` is the odd one: ONE entry that EXPANDS into one input per
55
+ COLUMN of the linked table, so read them by column name (`inp("email")`), never by
56
+ the entry's own name. `hidden: ["created_at"]` drops columns from that expansion.
57
+ `input.list(element)` for arrays — wrap any element constructor, e.g.
58
+ `input.list(input.text())` or `input.list(input.object({ id: f.int() }))`. Prefer the
59
+ typed forms over `input.json()` when the shape is known.
60
+ **Typed inputs validate/coerce on bind, before your stack runs** — so reach for the
61
+ specific type instead of hand-rolling checks. `input.email({ required: true })` rejects a
62
+ malformed address with a 400 (and trims; add `methods: ["lower"]` to downcase) — no
63
+ `regex_matches` needed; `input.int`/`input.decimal`/`input.uuid`/`input.enum([...])`/`input.date`
64
+ likewise reject or coerce bad input at the boundary. Drop to `input.text` + `s.precondition`
65
+ only for rules no type expresses.
66
+ ⚠ `input.url` is NOT one of them — there is no engine `url` type, so it stores as `text`
67
+ and validates NOTHING: a `javascript:`/`data:` URL type-checks, imports, and binds. It
68
+ names intent and carries the `text` methods, nothing more. When the value gets navigated
69
+ to, check the scheme in the stack. It is INPUT ONLY — there is no `f.url` column.
70
+ ⚠ `s.precondition`'s `error` must be a TAGGED value — `c.text("…")`, not a bare string.
71
+ The engine falls back to the generic "Precondition failed." whenever it reads an empty or
72
+ non-scalar message, and a bare string lands there, so the client never sees your text. The
73
+ `error_type` → HTTP status mapping is correct either way; only the message is lost. The bare
74
+ form stays accepted so a pulled workspace round-trips, not as a spelling to choose.
75
+ `error_type` IS how a stack sets a response status: `badrequest`/`inputerror` → 400, `unauthorized` → 401, `accessdenied` → 403, `notfound` → 404, `toomanyrequests` → 429, `standard` → 500.
76
+ Normalizing transforms run on bind too — put `trim`/`lower`/`upper` on the input's `methods`
77
+ so `inp("name")` reads already-normalized; don't reroll `var $x = inp("name")|trim` in the stack.
78
+
79
+ - `f.text` — methods: alphaOk, digitOk, lower, max, min, ok, pattern, startsWith, trim, upper
80
+ - `f.int` — methods: max, min
81
+ - `f.decimal` — methods: max, min
82
+ - `f.bool`
83
+ - `f.uuid`
84
+ - `f.date`
85
+ - `f.email` — methods: lower, trim
86
+ - `f.password` — methods: max, min, minAlpha, minDigit, minLowerAlpha, minSymbol, minUpperAlpha, salt
87
+ - `f.json`
88
+ - `f.timestamp` (stored `epochms`)
89
+ - `f.image` (stored `blob_img`)
90
+ - `f.video` (stored `blob_video`)
91
+ - `f.audio` (stored `blob_audio`)
92
+ - `f.attachment` (stored `blob`)
93
+ - `input.file` — INPUT ONLY (no `f.` form)
94
+ - `input.dbLink` (stored `<tableGuid>_mvpschema`) — INPUT ONLY (no `f.` form)
95
+ - `f.geo.point` (stored `geo_point`)
96
+ - `f.geo.multipoint` (stored `geo_multipoint`)
97
+ - `f.geo.linestring` (stored `geo_linestring`)
98
+ - `f.geo.multilinestring` (stored `geo_multilinestring`)
99
+ - `f.geo.polygon` (stored `geo_polygon`)
100
+ - `f.geo.multipolygon` (stored `geo_multipolygon`)
101
+ - `f.enum`
102
+ - `f.vector` — methods: max, min
103
+ - `f.object` (stored `obj`)
104
+ - `f.tableRef` (stored `int`) — methods: max, min
@@ -0,0 +1,183 @@
1
+ # Filter catalog
2
+
3
+ > Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
4
+
5
+ Attach to a value with `withFilters(v, fl.name(...))` — the value `filters[]`
6
+ pipeline. Filters are passed spread (canonical); the array form
7
+ `withFilters(v, [fl.a(), fl.b()])` is also accepted. Every filter is typed:
8
+ the ones below carry named args, and the rest take NO arguments — call them
9
+ `fl.<name>()`, and passing an argument is a compile error.
10
+ A typed filter's declared argument list is an EXACT count, not a floor: passing more
11
+ than it lists THROWS, in the type and at runtime. The extra argument used to ride into
12
+ the filter's arg list, where the engine either ignores it or fails opaquely on a live
13
+ endpoint. `filter("name", …)` is the untyped escape for a filter the catalog
14
+ under-declares. Seven declared filters are exceptions the engine really does take more from, so they
15
+ stay variadic: `concat` (trailing separator), `index_by` (trailing list flag), `get` (trailing variable
16
+ map), `array_merge` and `array_merge_recursive` (any number of arrays), `jwe_encode` and
17
+ `jwe_decode`. To send more than a zero-argument filter's `()` accepts, use the same
18
+ `filter("name", …)` escape. A filter with NO declared list takes nothing and is emitted
19
+ `()` — except the variadic few listed under the catalog, which take arguments the
20
+ catalog never declared and so are not arity-checked at all.
21
+ A typed filter also accepts one object of NAMED arguments (`fl.add({ value: 1 })`); a key
22
+ the filter does not declare throws rather than being dropped.
23
+ A bare JS **scalar** — string, number or boolean — is accepted in ANY `fl.*` argument and
24
+ wrapped as the constant you would have written by hand (`fl.get("a.b", 0)` encodes
25
+ identically to `fl.get(c.text("a.b"), c.int(0))`). An object or array must still be built
26
+ with `c.obj`/`c.array`. The arg types below name each argument's ENGINE type, not the JS
27
+ type you may pass.
28
+ Read-modify-write a column from its current value with the pipeline: to increment
29
+ a counter you MUST `db.get` the row first, then pipe its bound value —
30
+ `col("clicks")` does NOT resolve to the stored value inside a `db.edit` `row`
31
+ (it is `null`, so `fl.add(1)` computes `null + 1` and the engine aborts):
32
+ `s.db.get({ table, fieldValue, as: "current" })` then
33
+ `s.db.edit({ table, fieldValue, row: { clicks: withFilters(ref("current.clicks"), fl.add(c.int(1))) } })`.
34
+ ⚠ This read-modify-write is NOT atomic — two concurrent writers can both read the
35
+ same value and one increment is lost. There is no dedicated atomic-increment
36
+ statement, and one CANNOT be synthesized in the SDK (it would compile to this same
37
+ `get` + `edit` pair). For a **concurrency-safe** counter, do the arithmetic in the
38
+ database with a single `s.db.direct_query` UPDATE (`SET clicks = clicks + 1 WHERE …`),
39
+ which the DB applies atomically. Reserve the pipeline form for low-contention counters
40
+ where a rare lost update is acceptable.
41
+ ⚠ `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface
42
+ does NOT expose: the engine derives a physical name from workspace + table ids (of the
43
+ form `x<workspace_id>_<table_id>`, e.g. `x6_203970`), ids assigned at import — not knowable
44
+ from a `table()` def (identity is a name + guid, not the numeric id), and `sql_name`
45
+ persists empty. So the safe counter drops out of the typed surface: hardcode
46
+ the physical name after inspecting the deployed table. A typed atomic path needs an
47
+ engine change.
48
+
49
+ - `fl.add(value: decimal): decimal`
50
+ - `fl.append(value: <T>, path: text): <T>[]`
51
+ - `fl.array_diff(value: <T>[]): <T>[]`
52
+ - `fl.array_diff_assoc(value: <T>[]): <T>[]`
53
+ - `fl.array_fill(start: int, count: int): any[]`
54
+ - `fl.array_fill_keys(keys: any[]): any[]`
55
+ - `fl.array_intersect(value: <T>[]): <T>[]`
56
+ - `fl.array_intersect_assoc(value: <T>[]): <T>[]`
57
+ - `fl.array_merge(value: <T>[]): <T>[]`
58
+ - `fl.array_merge_recursive(value: <T>[]): <T>[]`
59
+ - `fl.array_push(value: <T>): <T>[]`
60
+ - `fl.array_remove(value: <T>, path: text, strict?: bool): <T>[]`
61
+ - `fl.array_slice(offset: int, length?: int): <T>[]`
62
+ - `fl.array_unshift(value: <T>): <T>[]`
63
+ - `fl.base_convert(from_base: any, to_base: any): text`
64
+ - `fl.bitwise_and(value: int): int`
65
+ - `fl.bitwise_or(value: int): int`
66
+ - `fl.bitwise_xor(value: int): int`
67
+ - `fl.concat(value: any): text`
68
+ - `fl.contains(search: text): bool` — piped value is the subject text; the arg is the substring searched for
69
+ - `fl.convert_encoding(to: text, from: text): text`
70
+ - `fl.create_object(values: <T>[]): any`
71
+ - `fl.crypto_jwe_decode(check_claims: json, key: json, key_algorithm: enum, content_algorithm: enum, timeDrift?: int): json`
72
+ - `fl.crypto_jwe_encode(headers: json, key: json, key_algorithm: enum, content_algorithm: enum, ttl?: int): text`
73
+ - `fl.crypto_jws_decode(check_claims: json, key: json, algorithm: enum, timeDrift?: int): json`
74
+ - `fl.crypto_jws_encode(headers: json, key: json, algorithm: enum, ttl?: int): text`
75
+ - `fl.csv_create(rows: text[], separator: text, enclosure: text, escape: text): text` — the header-writing counterpart to `csv_encode`: the PIPED value is the list of column names (written as the header line) and `rows` carries the data rows
76
+ - `fl.csv_decode(separator: text, enclosure: text, escape: text): any`
77
+ - `fl.csv_encode(separator: text, enclosure: text, escape: text): text` — writes NO header — values only, each row in THAT row's key order with no normalization across rows, so rows whose keys differ in order or count silently misalign columns. Nested cells are JSON-encoded and `false` writes empty. A piped array of SCALARS is treated as one row. Use `csv_create` for a header
78
+ - `fl.csv_parse(separator: text, enclosure: text, escape: text): any`
79
+ - `fl.decrypt(algorithm: enum, key: text, iv: text): text`
80
+ - `fl.detect_encoding(encodings?: text): text`
81
+ - `fl.div(value: decimal): decimal`
82
+ - `fl.encrypt(algorithm: enum, key: text, iv: text): text`
83
+ - `fl.ends_with(search: text): bool` — piped value is the subject text; the arg is the substring searched for
84
+ - `fl.epochms_add_ms(milliseconds: int): epochms`
85
+ - `fl.epochms_add_secs(seconds: int): epochms`
86
+ - `fl.epochms_date(format: text, timezone?: text): text`
87
+ - `fl.epochms_from_format(format: text, timezone?: text): text`
88
+ - `fl.epochms_transform(format: text, timezone?: text): text` — applies a relative shift (e.g. "+1 day") to the timestamp
89
+ - `fl.eq(value: <T>): bool`
90
+ - `fl.every(code: text, timeout?: int): any[]` — `code` is a JS body run per element (true for all?), over `$this`/`$index`/`$parent`
91
+ - `fl.filter(code: text, timeout?: int): any[]` — `code` is a JS body run per element (keep it? true/false), over `$this`/`$index`/`$parent`
92
+ - `fl.filter_empty(path?: text): <T>[]` — keeps entries that are not empty ("", null, 0, "0", false, [], {})
93
+ - `fl.filter_empty_array(path?: text): <T>[]`
94
+ - `fl.filter_empty_object(path?: text): <T>[]`
95
+ - `fl.filter_empty_text(path?: text): <T>[]`
96
+ - `fl.filter_false(path?: text): <T>[]`
97
+ - `fl.filter_null(path?: text): <T>[]`
98
+ - `fl.filter_zero(path?: text): <T>[]`
99
+ - `fl.find(code: text, timeout?: int): any[]` — `code` is a JS body run per element; returns the first element it accepts
100
+ - `fl.findIndex(code: text, timeout?: int): any[]` — `code` is a JS body run per element; returns the first matching index
101
+ - `fl.first_notempty(value: any): any` — first value that is not empty ("", null, 0, "0", false, [], {})
102
+ - `fl.first_notnull(value: any): any`
103
+ - `fl.fsort(path?: text, type?: "text"|"itext"|"natural"|"inatural"|"number", asc?: bool): <T>[]` — `type` is the comparator, and ONLY "number" compares numerically — "text"/"itext" are strcmp/strcasecmp, "natural"/"inatural" are the human-readable "a2 < a10" orderings. Default "itext". Anything else silently sorts as text, so a numeric sort MUST spell "number"; the path arg drills into each element
104
+ - `fl.get(path: text, default?: json): any`
105
+ - `fl.gt(value: <T>): bool`
106
+ - `fl.gte(value: <T>): bool`
107
+ - `fl.has(path: text): bool`
108
+ - `fl.hmac_md5(key: text, raw?: bool): text`
109
+ - `fl.hmac_sha1(key: text, raw?: bool): text`
110
+ - `fl.hmac_sha256(key: text, raw?: bool): text`
111
+ - `fl.hmac_sha384(key: text, raw?: bool): text`
112
+ - `fl.hmac_sha512(key: text, raw?: bool): text`
113
+ - `fl.icontains(search: text): bool` — case-insensitive; piped value is the subject, the arg is the substring
114
+ - `fl.iends_with(search: text): bool` — case-insensitive; piped value is the subject, the arg is the substring
115
+ - `fl.in(search: <T>): bool`
116
+ - `fl.index_by(path: text): { [key: string]: <T>[] }` — a GROUP-BY: every value is an ARRAY of the items sharing that key, even when only one does, so a lookup reads `idx[key][0]`. Items whose path is missing or non-scalar are dropped
117
+ - `fl.istarts_with(search: text): bool` — case-insensitive; piped value is the subject, the arg is the substring
118
+ - `fl.join(separator: text): text`
119
+ - `fl.jwe_decode(arg1: any)`
120
+ - `fl.jwe_encode(arg1: any)`
121
+ - `fl.lambda(code: text, timeout?: int): any` — runs a JS body once over the piped value, which it binds as `$this` (NOT `$parent`)
122
+ - `fl.log(base: <T>): decimal`
123
+ - `fl.lt(value: <T>): bool`
124
+ - `fl.lte(value: <T>): bool`
125
+ - `fl.ltrim(mask?: text): text`
126
+ - `fl.map(code: text, timeout?: int): any[]` — `code` is a JS body run per element, over `$this`/`$index`/`$parent` — build it with `lam.fn`
127
+ - `fl.md5(raw?: bool): text`
128
+ - `fl.mod(value: int): int`
129
+ - `fl.mul(value: decimal): decimal`
130
+ - `fl.ne(value: <T>): bool`
131
+ - `fl.num_max(value: any): decimal`
132
+ - `fl.num_min(value: any): decimal`
133
+ - `fl.number_format(decimals: int, decimal_separator: text, thousands_separator: text): string`
134
+ - `fl.pick(keys: text): <T>`
135
+ - `fl.pow(exp: <T>): decimal`
136
+ - `fl.prepend(value: <T>, path: text): <T>[]`
137
+ - `fl.range(start: int, stop: int): int[]`
138
+ - `fl.reduce(initial_value: int, code: text, timeout?: int): any[]` — `code` is a JS body run per element; the ACCUMULATOR is `$result` (there is no `$acc`) and `initial_value` is REQUIRED — omitting it would slot the code as the initial value
139
+ - `fl.regex_match(subject: text): text[]`
140
+ - `fl.regex_match_all(subject: text): text[]`
141
+ - `fl.regex_quote(delimiter?: text): text`
142
+ - `fl.regex_replace(replacement: text, subject: text): text`
143
+ - `fl.regex_test(subject: text): bool`
144
+ - `fl.round(precision?: int): decimal`
145
+ - `fl.rtrim(mask?: text): text`
146
+ - `fl.secureid_decode(salt: text): int`
147
+ - `fl.secureid_encode(salt: text): text`
148
+ - `fl.set(path: text, value: any): any`
149
+ - `fl.set_conditional(path: text, value: any, conditional: any): any`
150
+ - `fl.set_ifnotempty(path: text, value: any): any`
151
+ - `fl.set_ifnotnull(path: text, value: any): any`
152
+ - `fl.sha1(raw?: bool): text`
153
+ - `fl.sha256(raw?: bool): text`
154
+ - `fl.sha384(raw?: bool): text`
155
+ - `fl.sha512(raw?: bool): text`
156
+ - `fl.some(code: text, timeout?: int): any[]` — `code` is a JS body run per element (true for any?), over `$this`/`$index`/`$parent`
157
+ - `fl.split(separator: text): text[]`
158
+ - `fl.starts_with(search: text): bool` — piped value is the subject text; the arg is the substring searched for
159
+ - `fl.string_replace(search: text, replacement: text): text`
160
+ - `fl.strip_tags(exclude?: text): text`
161
+ - `fl.stripos(search: text): int`
162
+ - `fl.strpos(search: text): int`
163
+ - `fl.sub(value: decimal): decimal`
164
+ - `fl.substr(start: int, length: int): text`
165
+ - `fl.to_epoch_day(timezone?: text): int`
166
+ - `fl.to_epoch_hour(timezone?: text): int`
167
+ - `fl.to_epoch_minute(timezone?: text): int`
168
+ - `fl.to_epoch_ms(timezone?: text): int`
169
+ - `fl.to_epoch_sec(timezone?: text): int`
170
+ - `fl.to_epochms(timezone?: text): epochms`
171
+ - `fl.transform(expression: text): any` — `expression` is Xano Expression Engine source, NOT a JS body — no `return`, and the piped value is `$0` (or `$$`), NOT `$this` (which is null here). `$var`/`$input`/`$env`/`$auth` resolve and filters pipe inside it: `$0 * 2`, `$0|sort|join:","`. Parenthesize a pipe inside an object literal — `{ s: ($0|sort|join:",") }` — or its comma is read as the key separator and later keys vanish silently. For JavaScript use `lambda`
172
+ - `fl.trim(mask?: text): text`
173
+ - `fl.unique(path?: text): <T>[]`
174
+ - `fl.unpick(keys: text): <T>` — returns the object without the named keys (inverse of a pick)
175
+ - `fl.unset(path: text): any`
176
+ - `fl.url_addarg(key: text, value: text, encoding_rfc3986?: bool): text`
177
+ - `fl.url_delarg(key: text): text`
178
+ - `fl.url_getarg(key: text, default?: text): text`
179
+ - `fl.url_hasarg(key: text): text`
180
+
181
+ Zero-argument filters — call as `fl.<name>()`; an argument is a compile error: abs, acos, acosh, addslashes, array_entries, array_keys, array_pop, array_shift, array_shuffle, array_values, asin, asinh, atan, atanh, avg, base64_decode, base64_decode_urlsafe, base64_encode, base64_encode_urlsafe, bin2hex, bindec, bitwise_not, capitalize, ceil, cos, count, create_object_from_entries, decbin, dechex, decoct, deg2rad, empty, escape, even, exp, first, flatten, floor, from_utf8, hex2bin, hexdec, is_array, is_bool, is_decimal, is_int, is_object, is_text, is_uuid, json_decode, json_encode, last, list_encodings, ln, log10, lower, max, min, not, null, octdec, odd, product, querystring_parse, rad2deg, reverse, safe_array, sin, sql_alias, sql_esc, sqrt, strip_accents, strlen, sum, tan, text_escape, text_unescape, to_bool, to_decimal, to_expr, to_int, to_text, to_utf8, uid, upper, url_decode, url_decode_rfc3986, url_encode, url_encode_rfc3986, url_parse, uuid4, xml_decode, yaml_decode, yaml_encode.
182
+
183
+ Variadic filters — they take arguments, but no declared list, so the count is not checked: sort, sprintf.
@@ -0,0 +1,11 @@
1
+ # Agent and MCP def shapes
2
+
3
+ > Read when the workspace defines an `agent()` or an `mcpServer()`.
4
+
5
+ - `mcpServer({ name, guid?, description?, instructions?, docs?, enabled?, canonical?, spec?, tags?, history?, tools?, llm?, output? })` — an MCP toolset. `llm?`/`output?` are the same blocks `agent()` takes and are usually absent: an MCP server and an agent are ONE stored object distinguished by `type`, so a server that carries LLM settings can say so. Returns a handle with `getPath()`/`getUrl(baseUrl)` for the Streamable-HTTP endpoint — `getUrl` is NOT idempotent, a `baseUrl` already carrying an `/x2/mcp/<…>/stream` path (an earlier result) THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral; does NOT fire in the sandbox (see Gotchas).
6
+ - `tools?`: a `ToolsetToolEntry[]`. Pass the `tool()` HANDLES directly (`tools: [saveNote]`), like every other collection in the SDK; use the `{ tool, enabled?, auth? }` wrapper only when a tool needs `enabled: false` or per-tool `auth`. `auth` names an auth **table** (a `table({ auth: true })` handle or its name) — Xano's ONLY MCP auth surface (per-tool; there is no server-level gate). An entry that names no tool (no handle, no `id`) THROWS at export rather than emitting the `id: 0` null reference it used to; a deliberate raw `id: 0` warns and is carried through, so a pulled workspace still round-trips.
7
+ - `agent({ name, guid?, description?, docs?, enabled?, canonical?, tags?, history?, llm, tools?, output? })` — an LLM orchestrator. No top-level `instructions`/`prompt`/`spec` — the prompt lives under `llm`. Invoke from a stack with `s.ai.agent.run({ agent, args })`.
8
+ - `llm` (REQUIRED): typed provider settings, a discriminated union on `type` (`"xano-free" | "anthropic" | "openai" | "google-genai"`). Shared fields: `systemPrompt?`, `maxSteps?` (default `5`), and `prompt?` XOR `messages?` (genuinely exclusive: both is a type error and throws — the engine stores ONE `prompt_type`, so one would be dropped); plus provider fields (`apiKey?`, `model?`, `temperature?`, `reasoningEffort?`, …). String fields accept Twig placeholders — `{{ $args.x }}` for run inputs (the `args` of `s.ai.agent.run`), `{{ $env.NAME }}` for env vars.
9
+ - `tools?`: same `ToolsetToolEntry[]` as `mcpServer` — bare `tool()` handles, or a `{ tool, enabled?, auth? }` wrapper when one needs `enabled`/`auth`.
10
+ - `output?`: `{ schema: Record<string, input.*>, enabled? }` — structured-output schema. `schema` is a named-field record authored with the `input.*` catalog, exactly like a `defineFunction`/`query` `input:` map (the stored `structuredOutputsSchema` is the same wire shape as function inputs). e.g. `output: { schema: { priority: input.enum(["low","high"]), summary: input.text() } }`. When you pass the agent *handle* to `s.ai.agent.run({ agent })`, `.result` is typed straight from this schema — no `resultShape` witness needed (the shape is declared once). `resultShape` remains only to override that, or to type an agent referenced by bare name.
11
+ - **Run + read recipe (`s.ai.agent.run`):** bind the run to a var (`s.ai.agent.run({ agent, args, as: "run" })`) — it produces a rich envelope, and the completion is at **`.result`**. Read one structured field with a dotted ref (`response: ref("run.result.priority")`) or the whole completion (`ref("run.result")`, typed from `output.schema`); persist it in a later step the same way (`s.set_var({ name, value: ref("run.result.summary") })`). `args` is a plain object of run inputs (`{ topic: inp("topic") }` — raw literals are fine, e.g. `{ max_steps: 3 }`) surfaced to the agent as `{{ $args.topic }}`. Tool-call / per-step data (when tools ran) rides `.toolCalls` / `.steps` on the same envelope — both optional, absent or empty when no tools executed.
@@ -0,0 +1,68 @@
1
+ # Core def shapes
2
+
3
+ > Read when authoring a function, query, api group, task, workflow test, middleware, or tool — and for the `response` and `expr` shapes every one of them uses.
4
+
5
+ The def-object passed to each factory. `?` = optional. `input` is keyed by
6
+ input name (`input.<type>(opts?)`); `stack` is `Statement[]` (`s.*`); `response`
7
+ is a `ResponseDef` (see **Responses** below). Object identity is `guid?` —
8
+ omit it and it derives from `name` (set it to survive a rename).
9
+
10
+ - `defineFunction({ name, guid?, description?, docs?, workspace?, input?, stack?, response?, tests? })`
11
+ - `query({ name, verb, apiGroup?, guid?, auth?, input?, stack?, response?, responseType?, apiEnabled?, disabled?, cache?, description?, docs?, tests?, example? })`
12
+ - `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else — most often a lowercase `"post"` — makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`
13
+ - `apiGroup`: an `apiGroup()` def handle (or its name) — binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.
14
+ - `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) — any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.
15
+ - `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
16
+ - **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand. `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a value that IS `.`/`..` (a URL parser drops those before routing — `%2e` counts — addressing a different endpoint). The keys are typed from the literal `name`, so a typo is a compile error. The HANDLE's `q.toSearchParams(input)` drops path params for a GET; the free `query.toSearchParams(input)` has no view of the route and keeps every key.
17
+ - `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors? })` — a query container; register it and bind queries to it via their `apiGroup`.
18
+ - `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.
19
+ - `mode?`: `"default"` (the default) | `"custom"` | `"disabled"`, lowercase — a fourth value THROWS at export (`apiGroup()` itself does not check), because Xano neither rejects nor blanks it: it DROPS THE WHOLE API GROUP on import, so the deploy succeeds and every query in the group 404s. ⚠ Every OTHER field applies only under `"custom"`: `"default"` serves a FIXED permissive policy (any origin, `allow-headers: *`, `allow-credentials: true`, `max-age: 86400`) and ignores the block, so setting `maxAge`/`allowCredentials`/`allowHeaders` alone changes nothing. `"disabled"` sends no CORS headers at all, so every browser call fails.
20
+ - ⚠ Under `"custom"`, `allowOrigins` is matched as EXACT strings (scheme+host+port, no wildcard or subdomain expansion) and `"*"` is compared as a literal origin — it matches NOTHING. An unmatched origin gets no `access-control-*` headers at all, so the call fails in the browser on a missing `access-control-allow-origin` while export, deploy and the preflight all look fine. Name each origin, or use `mode: "default"` for any-origin. `allowMethods` gates the REAL response too: a verb left off gets no CORS headers back even though its preflight passes. Export warns on an empty origin list, a `"*"` entry, and a policy with no method enabled.
21
+ - `defineFunction`/`query`/`apiGroup` above cover the queries+tables core; the four below are the "reach past that" primitives (tasks, workflow tests, middleware, tools). Agents and MCP servers are the same family and live in `llms/kinds-agent-mcp.md`. Same envelope conventions (`guid?`, `description?`, `docs?`, `tags?`, `history?`) unless noted.
22
+ - `task({ name, guid?, description?, docs?, datasource?, active?, tags?, history?, schedule?, stack?, middleware? })` — a scheduled background job (function-like `stack`, no `input`/`response`).
23
+ - `schedule?`: a `ScheduleDef[]` (NOT a single object) — `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`. `startsOn`/`endsOn` are **timestamp strings** validated at encode time — `"2026-01-01T00:00:00Z"`, or the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries — never epoch numbers, and never zoneless; `freq` is the repeat interval **in seconds** (default `86400` = daily); `endsOn` present ⇒ the schedule has an end. `endsEnabled?` defaults to that and is recovery-only — state it to reproduce a stored schedule that remembers an end date with the gate OFF. Fires on an ephemeral; does NOT fire in the sandbox (see Gotchas).
24
+ - `workflowTest({ name, guid?, description?, docs?, datasource?, active?, tags?, stack? })` — an end-to-end test. NO `input`/`response`: `.call` something with an `as`, then assert on that var — `s.function.call({ fn, input, as: "r" })`, `s.expect.to_equal({ expr: ref("r"), value: c.int(42) })`. `s.expect.*` belongs here — it is not inert elsewhere (a failure 500s the request), so treat one in a query/function/task as a mistake to remove. `active?` defaults `true`; chain tests with `s.workflow_test.call({ workflowTest: <def handle> })`.
25
+ - `datasource?`: **the trap.** Default `""` is an EMPTY datasource (recommended), not "no datasource". Any non-empty name makes the engine CLONE it before EVERY run — against production-sized data, slow enough to fail the run. `"live"` warns at compile time; other names don't.
26
+ - `middleware({ name, guid?, description?, docs?, resultStrategy?, exceptionPolicy?, tags?, history?, input?, stack?, response?, responseShape?, tests? })` — a pre/post interceptor (function-like `stack`); attach it via a host's `middleware: { pre, post }`. ⚠ `input` ENCODES but an ATTACHED middleware never has it bound — the host request binds its own inputs, so `inp()` inside pre/post fails at runtime with `Unable to locate input` and a declared default does not stand in (`export()` warns). Read the request body with `s.util.get_all_input` instead; it yields a `{ type, vars }` envelope. `s.middleware.call` is the one path that DOES bind the declared map.
27
+ - `resultStrategy?`: `"merge" | "replace"` (default `merge`) — how the middleware `response` folds into the host's.
28
+ - `exceptionPolicy?`: `"silent" | "rethrow" | "critical"` (default `"rethrow"` — a throw ABORTS the request and surfaces the authored error/status, which is what a guard wants). `"silent"` swallows the throw and lets the request through, so a guard set to it is NOT enforced — use it only for advisory middleware. `"critical"` is `"rethrow"` plus skipping the `post` chain.
29
+ - `tool({ name, guid?, description?, instructions?, docs?, enabled?, tags?, history?, input?, stack?, response?, responseShape?, middleware? })` — a function-like operation (`input`/`stack`/`response`) that a toolset (MCP server or agent) exposes. Register it, then reference it from a toolset's `tools`.
30
+ ### Responses
31
+
32
+ The `response?` field (on functions, queries, tools, middleware, and
33
+ response-bearing triggers) maps to the stored `result[]`:
34
+
35
+ - `ResponseDef = Value | Record<string, Value>`.
36
+ - A single `Value` → one unnamed result item: `response: ref("rows")`.
37
+ - A record → one named item per key: `response: { user: ref("u"), token: ref("t") }`.
38
+ - Omitted → empty `result[]` (no response body).
39
+
40
+ ### Expressions (`expr`)
41
+
42
+ `expr(left, op, right)` builds the comparison used by every condition/`where`
43
+ surface — `s.conditional`/`s.while` `when` (incl. each `elif` branch), and
44
+ `db.query` `where`/`additionalWhere` (and the search triggers) — one shared tree.
45
+
46
+ - `op`: `=`, `!=`, `>`, `<`, `>=`, `<=` (JS aliases `==` `===` `!==` are accepted and normalized).
47
+ - `left`/`right` are `Value`s — `col("x")` (a table column), `ref`, `inp`, `auth(...)`, or `c.*`.
48
+ - For the full operator set (`in`/`like`/`ilike`/`between`/`contains`/`overlaps`/`@>`/`~`/`search`/…)
49
+ use `cmp(left, op, right, { ignoreEmpty? })`; compose nested boolean logic with `and(...)`/`or(...)`.
50
+ - ⚠ The wider `cmp` operators are DATABASE-only (`where`, table view filter, db trigger
51
+ `search`). A RUNTIME condition — `s.conditional`/`elif`, `s.while`, `s.precondition`,
52
+ `array.*` `if` — takes the `expr` set only; the rest are refused at build time because
53
+ deployed they fail the request with `Invalid op: <op>` on that branch, usually a guard.
54
+ Spell membership out: `or(expr(x, "=", a), expr(x, "=", b))`.
55
+ - A condition/`where` accepts a single `expr(...)`/`cmp(...)`, an `and()`/`or()` group, an array of
56
+ those (ANDed), or (for `where`) a raw `Value`. `s.conditional`/`s.while`/`s.switch`, `db.query`,
57
+ `precondition`, and the `array.*` predicates all take the same TREE shape (operators per above).
58
+ - ⚠ `mixed(a, { or: b }, { and: c })` reproduces a container whose terms do NOT all join the
59
+ same way — the editor allows it, so pulled workspaces contain it. **Do not author it.** The
60
+ stored form does not record the grouping, and the two places it can appear disagree: a
61
+ branch (`s.conditional`/`s.while`/`precondition`) folds terms strictly left to right, so
62
+ `a OR b AND c` is `(a OR b) AND c`, while a `db.query` filter applies the engine's
63
+ AND-before-OR precedence and selects `a OR (b AND c)`. Write `and(or(a, b), c)` or
64
+ `or(a, and(b, c))` — each says one reading in every context. Pulls report these as
65
+ `ambiguous-condition`.
66
+ - A **filtered** operand (`withFilters(...)`) works inline in any condition/`where` (conditional,
67
+ while, `db.query`/addon, …) — e.g. `cmp(withFilters(col("title"), fl.trim()), "=", inp("q"))`.
68
+ - e.g. `db.query({ table: posts, where: expr(col("author"), "=", auth("id")), as: "rows" })`.