@xanots/sdk 0.0.11 → 0.0.13

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 (100) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +16 -9
  3. package/dist/.build-fingerprint +1 -1
  4. package/dist/{agent-file-refresh-QNKN5RYD.js → agent-file-refresh-GQWAAOBV.js} +4 -4
  5. package/dist/bin.js +11 -11
  6. package/dist/{branch-commands-2BLOC2GR.js → branch-commands-5KMPAAZV.js} +7 -7
  7. package/dist/bundle.d.ts +2 -2
  8. package/dist/bundle.js +2 -2
  9. package/dist/{capture-4WVJY4DQ.js → capture-YLUVAITI.js} +2 -2
  10. package/dist/{chunk-22TKBSDV.js → chunk-2AY3PKF4.js} +2 -2
  11. package/dist/chunk-2VTJSI6X.js +192 -0
  12. package/dist/{chunk-5XZ744TS.js → chunk-2ZCUO2UG.js} +1 -1
  13. package/dist/{chunk-XEOX6AM7.js → chunk-3LQGF2WS.js} +2 -2
  14. package/dist/{chunk-WP4OZZV4.js → chunk-3VFCKHOB.js} +2 -2
  15. package/dist/{chunk-XHEXOES3.js → chunk-5YDINUAP.js} +1 -1
  16. package/dist/{chunk-LU7TRWMC.js → chunk-6VNRMKFJ.js} +2 -2
  17. package/dist/{chunk-CTD5ZCV6.js → chunk-7WRJPKGK.js} +2 -2
  18. package/dist/{chunk-3INK4Y4E.js → chunk-AE3PDSDS.js} +1 -1
  19. package/dist/{chunk-DBFU47BJ.js → chunk-AOFFKSJC.js} +2 -2
  20. package/dist/{chunk-TCFIPDB3.js → chunk-AVGDL6RB.js} +1 -1
  21. package/dist/{chunk-UOZMSF4C.js → chunk-BYQHCCYU.js} +5 -5
  22. package/dist/{chunk-BC2C5GVI.js → chunk-DCMANKMX.js} +1 -1
  23. package/dist/{chunk-WHOJWOSV.js → chunk-EETVJZAZ.js} +1 -1
  24. package/dist/{chunk-VNQM3V2C.js → chunk-EXENFOWE.js} +2 -2
  25. package/dist/{chunk-AIZKXUNP.js → chunk-IMLYGQK6.js} +2 -2
  26. package/dist/{chunk-OWGCOGKK.js → chunk-MEFMTICH.js} +83 -8
  27. package/dist/{chunk-QK7ZQJLP.js → chunk-N74KDCBD.js} +39 -25
  28. package/dist/{chunk-3IGNIP6R.js → chunk-NDP7OUPS.js} +1 -1
  29. package/dist/{chunk-4Q7ZOHH7.js → chunk-OONT4ZL4.js} +3 -3
  30. package/dist/{chunk-EQW3YT5U.js → chunk-P6PVBQL6.js} +2 -2
  31. package/dist/{chunk-QYMAZRAU.js → chunk-PJNWOZMT.js} +5 -5
  32. package/dist/{chunk-BSK7ELHU.js → chunk-PR7OXHGZ.js} +1 -1
  33. package/dist/{chunk-RCT7UX7B.js → chunk-RLI6XD4O.js} +32 -27
  34. package/dist/{chunk-ZQ2PKR6R.js → chunk-RQ3FXV4K.js} +2 -2
  35. package/dist/{chunk-Q77KNEUL.js → chunk-RQNMTDXD.js} +1703 -2
  36. package/dist/{chunk-4IF54NU5.js → chunk-S3DOJOW4.js} +41 -21
  37. package/dist/{chunk-HYBN4H3F.js → chunk-SS2V2QOG.js} +27 -20
  38. package/dist/{chunk-QKM4U5UK.js → chunk-TJS2AF5Y.js} +2 -2
  39. package/dist/{chunk-DIA7CT7J.js → chunk-V5Y7D4LH.js} +11 -2
  40. package/dist/{chunk-KA6G2L7U.js → chunk-VK26K7AY.js} +3 -3
  41. package/dist/{chunk-VAF6A3YD.js → chunk-XWFRNJMQ.js} +1 -1
  42. package/dist/{chunk-7ZYW652H.js → chunk-YHS6VVLJ.js} +2 -2
  43. package/dist/{chunk-G4EJMQLD.js → chunk-YX22LKQE.js} +2 -2
  44. package/dist/{chunk-F6CYJ7TN.js → chunk-Z2ZIE5CO.js} +2 -2
  45. package/dist/cli.d.ts +13 -6
  46. package/dist/cli.js +10 -10
  47. package/dist/codegen-command-ZDHMGFWZ.js +47 -0
  48. package/dist/codegen.d.ts +5 -5
  49. package/dist/codegen.js +2 -2
  50. package/dist/{completion-WF46272M.js → completion-HJU5QEFB.js} +2 -2
  51. package/dist/{deploy-command-IP7V7GT4.js → deploy-command-CGVKRWUE.js} +19 -19
  52. package/dist/{ephemeral-command-U4AQ3TXX.js → ephemeral-command-2NKPEXUU.js} +8 -8
  53. package/dist/index.d.ts +88 -104
  54. package/dist/index.js +9 -9
  55. package/dist/init-command-DNDONP3O.js +32 -0
  56. package/dist/internal.d.ts +10 -16
  57. package/dist/internal.js +34 -34
  58. package/dist/{io-P2H75UV2.js → io-UBDMMDH6.js} +3 -3
  59. package/dist/{live-diff-IXKBVG4K.js → live-diff-HCOTN5WC.js} +3 -3
  60. package/dist/{lock-46FWYE4D.js → lock-HQ4KARU2.js} +2 -2
  61. package/dist/{lock-commands-ZZKZ4LZJ.js → lock-commands-6U7UIJGR.js} +11 -11
  62. package/dist/{login-command-Z6CHTA57.js → login-command-P7LXD5TE.js} +6 -6
  63. package/dist/{loop-D5NPL4VH.js → loop-IC5ISSYB.js} +3 -3
  64. package/dist/{marketplace-command-P4IPLJ6J.js → marketplace-command-T6JCHW7J.js} +4 -4
  65. package/dist/{meta-client-K2J4XH64.js → meta-client-LKRKR3L2.js} +4 -4
  66. package/dist/node.d.ts +6 -6
  67. package/dist/node.js +14 -14
  68. package/dist/{preflight-command-K346GPTY.js → preflight-command-UYE7SUQV.js} +16 -16
  69. package/dist/{profile-command-LJSBDV2L.js → profile-command-WBNSMQSI.js} +3 -3
  70. package/dist/{release-command-IMNTIVWK.js → release-command-CMMYT6XK.js} +26 -26
  71. package/dist/{response-BQVQ24l1.d.ts → response-D6xGLEIn.d.ts} +18 -23
  72. package/dist/{routes-manifest-PWZHDOI5.js → routes-manifest-MN6XBYRE.js} +25 -14
  73. package/dist/{runtime-V4C3AC3A.js → runtime-LSLIDALK.js} +1 -1
  74. package/dist/scaffold.d.ts +16 -8
  75. package/dist/scaffold.js +7 -11
  76. package/dist/{static-host-3WMV7IZO.js → static-host-4JDQCHUW.js} +1 -1
  77. package/dist/{status-command-AL47VG7H.js → status-command-VARULAR5.js} +4 -4
  78. package/dist/{store-BLyNeQ8S.d.ts → store-9Psd0jiF.d.ts} +115 -102
  79. package/dist/{test-command-YAZLKLGQ.js → test-command-YCAR4O35.js} +7 -7
  80. package/dist/{upgrade-command-BN3EHAOI.js → upgrade-command-A75DOIUH.js} +15 -16
  81. package/dist/{workspace-2COHDBM3.js → workspace-33KHFLX3.js} +2 -2
  82. package/dist/{workspace-command-MNK7Y7MQ.js → workspace-command-YELP47SJ.js} +26 -28
  83. package/dist/{workspace-export-DURY5WYL.js → workspace-export-MLYTYZS5.js} +3 -3
  84. package/dist/{xdo-BjJj5W_E.d.ts → xdo-ODuJklk6.d.ts} +27 -23
  85. package/guides/authoring.md +11 -2
  86. package/guides/typed-frontend.md +23 -6
  87. package/llms/kinds-realtime.md +2 -2
  88. package/llms/statements-data.md +3 -2
  89. package/llms/tests.md +1 -1
  90. package/llms/triggers.md +2 -2
  91. package/llms/values.md +1 -1
  92. package/llms-full.txt +26 -26
  93. package/llms.txt +17 -18
  94. package/manifest.json +6 -2
  95. package/package.json +1 -1
  96. package/dist/chunk-ANUDXFEX.js +0 -881
  97. package/dist/chunk-JGCWTCA7.js +0 -95
  98. package/dist/chunk-YBC3IKMF.js +0 -845
  99. package/dist/codegen-command-FUT2KJB6.js +0 -49
  100. package/dist/init-command-NPVL32L6.js +0 -34
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # xanots v0.0.11
1
+ # xanots v0.0.13
2
2
 
3
3
  > TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
4
4
 
@@ -16,27 +16,26 @@ caller, `c.*` a constant — resolved at request time; JS operators over them do
16
16
  compute (see Gotchas). Requests share no memory — state persists in tables or redis.
17
17
 
18
18
  Coverage: object kinds 25/31, statement surfaces 214/214, filters 226 (226 typed).
19
- Not authorable here: branch, market_item, realtime_channel, run.job, run.service, tablemap — these cannot be authored and do not survive a pull; see `coverage.objectKinds.unmodeled` in `manifest.json`.
19
+ Not authorable here: branch, market_item, realtime_channel, run.job, run.service, tablemap — these cannot be authored and do not survive a pull; reasons in `coverage.objectKinds.unmodeled`.
20
20
 
21
21
  This file is the whole always-read surface: the mental model, the deploy contract,
22
22
  every gotcha, and control flow. Per-surface detail lives in the topic files listed
23
- below — open the one whose condition matches the task, and skip the rest. For
24
- exhaustive per-entry detail in NEITHER — a statement's full field schema with engine
25
- defaults, a filter's complete argument list, the engine `storedName` mapping — do a
26
- TARGETED lookup in the shipped `manifest.json` (a program imports it as
27
- `@xanots/sdk/manifest.json`, which Node ESM needs `with { type: "json" }` on;
28
- grep or `jq` the one entry you need;
29
- it is ~65k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
23
+ below — open the one whose condition matches the task, skip the rest. For
24
+ exhaustive per-entry detail in NEITHER — a statement's field schema with engine
25
+ defaults, a filter's full argument list, the `storedName` mapping — do a TARGETED
26
+ lookup in the shipped `manifest.json` (a program imports it as
27
+ `@xanots/sdk/manifest.json`, needing `with { type: "json" }` in Node ESM;
28
+ it is ~60k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
30
29
  jq '.statements[] | select(.sPath=="db.get")' manifest.json
31
30
  jq '.filters[] | select(.name=="json_decode")' manifest.json
31
+ ⚠ `fields` is null on the 65 `declarative: false` statements (`db.get`, …): typed wrappers whose arguments are the factory's `.d.ts` signature. Null ≠ missing.
32
32
  Select a statement on `sPath` (the `s.*` path you write), NOT `surface` (the XanoScript term): 24 of 214 differ — `var`→`set_var`, `break`→`foreach_break`, `foreach.remove`→`foreach_remove`, and every `expect.*`.
33
33
 
34
34
  ## Topic files
35
35
 
36
- Each line is a condition on the task. Open the files whose condition matches and
37
- skip the rest paths are relative to this file (`node_modules/@xanots/sdk/` once
38
- installed), so a plain file read resolves them at the version you have.
39
- `llms-full.txt` is everything concatenated — one fetch, for a reader that cannot open files.
36
+ Paths are relative to this file (`node_modules/@xanots/sdk/` once installed), so a
37
+ plain file read resolves them at the version you have.
38
+ `llms-full.txt` is everything concatenated one fetch for a reader that cannot open files.
40
39
 
41
40
  - [Object kinds](llms/object-kinds.md): Read when deciding WHAT to build — every authorable primitive (tables, endpoints, functions, tasks, agents, MCP, realtime, microservices, …), what each is, and which factory + register method build it. Read it too when a `register*` call will not typecheck because the array was built by `.flatMap()`/`.concat()` across modules.
42
41
  - [Core def shapes](llms/kinds-core.md): 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.
@@ -336,11 +335,11 @@ Non-obvious authoring rules:
336
335
  no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
337
336
  `getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
338
337
  the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
339
- ⚠ A FLOOR — **~289 kB minified (~57 kB gzipped)** for the FIRST def; splitting modules
340
- never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
341
- one, adds ~1 kB so reducing what a def does will not reduce it.
338
+ ⚠ A FLOOR — **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules
339
+ never removes it. The floor is the RUNTIME, not the def: a second or much richer def
340
+ adds ~2 kB, so trimming a def does not shrink it.
342
341
  Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
343
- plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
342
+ plain data importing NOTHING, still compile-checked: `routePath("GET blog/{slug}", { slug })`
344
343
  `channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
345
344
  URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
346
345
  - **Intra-workspace imports use `.js` specifiers** (`../tables/links.js`), not
@@ -417,7 +416,7 @@ Non-obvious authoring rules:
417
416
  submission on bind, so `s.security.check_password` compares two different hashes
418
417
  and a correct password always fails (`ok:false` on a found row). Take the submitted
419
418
  password as `input.text()` on both signup and login and pass the plaintext straight
420
- to `check_password` (which does the comparison hash itself).
419
+ to `check_password` (which does the comparison hash itself). `export()` warns.
421
420
  - **Agents authenticate with env vars — never `xanots login`.** `login` blocks on a
422
421
  browser consent no agent can complete. Set `$XANO_INSTANCE_URL` + `$XANO_WORKSPACE_ID`
423
422
  + `$XANO_META_TOKEN` and run `deploy`/`release` directly: no disk, no rotation, so it
@@ -629,7 +628,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
629
628
 
630
629
  - The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs** and every `db` read misses. A test that buys seeded row 1 fails with its own precondition message, which reads as a wrong id rather than an empty database. Create what the test needs INSIDE the test — typically a `defineFunction` fixture the stack calls first.
631
630
  - `s.api.call` does NOT raise when the endpoint answers with an error. It BINDS the error envelope (`{code, message}`) to its `as` and carries on, so a later `s.expect.to_be_defined({ expr: ref("r.field") })` reports the ASSERTION while the real failure was the call, four statements up. Assert on the envelope — `s.expect.to_contain({ expr: ref("r.code"), value: c.text("ERROR_CODE_INPUT_ERROR") })` — when a call may fail. `s.function.run` raises instead; the two disagree.
632
- - `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. `exception` is a `Value` whose text the raised message must CONTAIN (`c.text("already exists")`, not a bare string); omit it to accept any error.
631
+ - `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. An outer one raises `Missing var entry: <name>` there, which the test reports as `to_throw` not matching (`export()` warns). `exception` is a `Value` whose text the raised message must CONTAIN (`c.text("already exists")`, not a bare string); omit it to accept any error.
633
632
  - `s.expect.to_throw` catches such a call only when the error carries a MESSAGE. `ERROR_CODE_ACCESS_DENIED` arrives with an empty one, so `to_throw` around an auth-refused call reports `to_throw failed - response is ok` — which reads as a broken auth gate on a gate that works.
634
633
  - An endpoint's `auth` gate is NOT enforced on `s.api.call`. A `query({ auth: users })` runs anyway and fails only where its stack dereferences `auth(...)`. A stack that never touches `auth(...)` runs unauthenticated and passes.
635
634
  - Neither `auth.token` nor an `Authorization` entry in `headers` authenticates the call — a token that answers 200 over real HTTP is refused here. To cover auth-gated logic, move the body into a `defineFunction` taking the user id and `s.function.call` that; the gate itself is not reachable from a workflow test.
@@ -701,11 +700,11 @@ The run is isolated in ways that make a correct test fail for reasons the failur
701
700
  - `deliverTo?`: `"channel"` (default) | `"sender"` | `"others"` | `"explicit"`. ⚠ `"explicit"` still delivers to NOBODY — nothing selects recipients from inside a handler, and `s.realtime.publish` (which originates an event INTO a channel) is not a substitute.
702
701
  - Only `"channel"`/`"others"` fan out AND are written to the `conversation` transcript — a `"sender"` response is invisible to every future joiner.
703
702
  - **Both input surfaces read as ordinary inputs:** `inp("body")` for a payload field, `inp("room_id")` for the channel's `{room_id}`. No session lookup, no frame parsing.
704
- - A path param is bound ONCE at join and read from the connection thereafter, never from the frame — a sender cannot claim a room it did not join. The same values reach a channel `join`/`leave` trigger's stack.
703
+ - A path param is bound ONCE at join and read from the connection thereafter, never from the frame — a sender cannot claim a room it did not join.
705
704
  - `s.realtime.get_session({ as })` — the CALLER's realtime session for the current frame. FLAT shape:
706
705
  - `authenticated` bool · `client_id` text (the AUTHED ROW ID as text, `""` anonymous) · `dbo_id` int (the auth TABLE's id — NOT the user's row id; `0` anonymous — to look the caller up use `client_id`. `dbo_id` is an int in the same position and typechecks, so a gate that keys on it finds no user and refuses EVERYONE) · `socket_id` int (transport id) · `channel` text (resolved path, `""` in a server trigger) · `params` object (bound path params, `{}` when none — `ref("session.params.room_id")`) · `extras` object · `opened_at` decimal.
707
706
  - Works in a realtime MESSAGE stack and in CHANNEL and SERVER trigger stacks; off that path it degrades to an anonymous session.
708
- - For a path param prefer `inp("room_id")`. Reach for the session when you need the CONNECTION (identity/extras) — "who is this sender" on an anonymous-client channel.
707
+ - For a path param prefer `inp("room_id")` in a MESSAGE; a lifecycle TRIGGER has only the session. Reach for the session when you need the CONNECTION (identity/extras) — "who is this sender" on an anonymous-client channel.
709
708
  - ⚠ THREE UNRELATED THINGS ARE CALLED A CLIENT ID: `session.client_id` (app-facing identity), `session.socket_id` (transport), and a frame's `options.client_id` (the at_least_once CURSOR handle). Conflating the first and last breaks at_least_once for anonymous clients.
710
709
  - `s.realtime.publish({ server, channel, data, message?, authTable?, authId? })` — the PUSH direction: originate a server-authored event onto a channel from ANY stack, no client frame first.
711
710
  - `server` is the handle or its NAME (resolved by name, not guid); `channel` is the FILLED-IN path (`channel.getChannel({ room_id: 42 })`), never the template — a constant still carrying `{param}` THROWS at author time, and a constant `server`/`channel` naming nothing this workspace registers WARNS at export.
@@ -773,8 +772,8 @@ realtimeChannelTrigger, mcpServerTrigger, agentTrigger, workspaceTrigger,
773
772
  errorTrigger}({ name, guid?, description?, active?, tags?, ... })`.
774
773
 
775
774
  - `tableTrigger({ name, table?, datasources?, actions?: {insert?,update?,delete?,truncate?}, stack })` — database/table trigger. `t.new` / `t.old` are the row **after** / **before** the change; `t.action` (`insert|update|delete|truncate`), `t.datasource`. Bind `table` to a `table()` handle and `t.new("col")` / `t.old("col")` are typed to that row (misspelled column = compile error). Nullability follows the enabled actions: insert → `old` is null, delete → `new` is null, update → both, truncate → neither. Config-only (no response).
776
- - `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` — realtime SERVER lifecycle (a client connecting to / disconnecting from the server, not a message). Inputs: `t.action` (`connect|disconnect`), `t.realtime_server`, `t.client`. Bind `realtimeServer` to a `realtimeServer()` handle (or its name). `connect` GATES the connection — a denial sends an `error` and CLOSES the socket with code 4401 before it is ever ready, so it is a real front door, not an observer; same return shape as a channel `join` (`{ allowed: c.bool(true) }` or any truthy value admits, EMPTY/FALSY DENIES — INCLUDING a gating trigger with NO `response`, which returns nothing and so refuses every client). A CRASH DENIES too — the transport seeds a deny and keeps it on a throw. Both failure modes lock the door, so plan for a self-inflicted LOCKOUT (an unguarded drill into a null `db.get` raises → everyone refused), not a breach. Gating is OPT-IN: a server with no `connect` trigger accepts every connection. `disconnect` is OBSERVATIONAL (return ignored, throws swallowed — cleanup must always complete). Both are SERVER-scoped, so `s.realtime.get_session` works but carries no channel path and no bound params.
777
- - `realtimeChannelTrigger({ name, channel, actions?: {join?,leave?,deliver?}, stack?, response?, responseShape? })` — realtime CHANNEL lifecycle. Inputs: `t.action` (`join|leave|deliver`), `t.channel`, `t.client`. Bind `channel` to a `realtimeChannel()` handle — a bare path is NOT accepted (it is unique only within its server). The three actions have DIFFERENT postures, and the posture decides what the stack should return: `join` GATES the join (it runs BEFORE the client becomes a member, so a denial means it never sees a fan-out) — return `{ allowed: c.bool(true) }` (optional `reason` reaches the client) or any truthy value to admit, and an EMPTY OR FALSY RETURN DENIES, so a stack that just falls through — or a gating trigger with NO `response` — refuses everyone, and a CRASH DENIES too. That is the inverse of a normal message (a crashing message still delivers) and of `deliver` below (a gate that fails OPEN). `join`/`leave` bind the channel's typed path params as INPUTS, so `inp("room_id")` resolves and the gate decides per room; a SERVER connect/disconnect has no channel, the one place a path param cannot be read; `leave` is OBSERVATIONAL (return ignored, throws swallowed); `deliver` GATES delivery PER RECIPIENT — the per-viewer redaction tool and the most expensive action here (a stack per recipient per message), and it needs `delivery.perRecipient` on the channel to run at all — BOTH HALVES are required, so a `deliver` trigger on a channel without the flag NEVER RUNS and every subscriber receives the UNREDACTED payload (no error, no log line); `export()` warns on each half alone. **`deliver`'s RETURN VALUES DO NOT READ LIKE A FILTER:** ONLY an explicit NULL drops the message for that recipient; an OBJECT replaces that recipient's payload; ANYTHING ELSE — INCLUDING `false`, `0`, `""` — DELIVERS IT UNCHANGED, as does a crash. So `return false` from a yes/no redaction check SENDS the message it was written to suppress — return null instead. The delivered payload arrives NESTED, so read `inp("payload").<field>`, and `t.client` is the SENDER while `s.realtime.get_session` describes the RECIPIENT this run is for.
775
+ - `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` — realtime SERVER lifecycle (a client connecting to / disconnecting from the server, not a message). Inputs: `t.action` (`connect|disconnect`), `t.realtime_server`, `t.client`. Bind `realtimeServer` to a `realtimeServer()` handle (or its name). `connect` GATES the connection — a denial sends an `error` and CLOSES the socket with code 4401 before it is ever ready, so it is a real front door, not an observer; same return shape as a channel `join` below (EMPTY/FALSY DENIES — INCLUDING a gating trigger with NO `response`, which returns nothing and so refuses every client). A CRASH DENIES too — a gate that cannot answer must not admit. Both failure modes lock the door, so plan for a self-inflicted LOCKOUT (an unguarded drill into a null `db.get` raises → everyone refused), not a breach. Gating is OPT-IN: a server with no `connect` trigger accepts every connection. `disconnect` is OBSERVATIONAL (return ignored, throws swallowed — cleanup must always complete). Both are SERVER-scoped, so `s.realtime.get_session` works but carries no channel path and no bound params.
776
+ - `realtimeChannelTrigger({ name, channel, actions?: {join?,leave?,deliver?}, stack?, response?, responseShape? })` — realtime CHANNEL lifecycle. Inputs: `t.action` (`join|leave|deliver`), `t.channel`, `t.payload`, `t.client`. Bind `channel` to a `realtimeChannel()` handle — a bare path is NOT accepted (it is unique only within its server). The three actions have DIFFERENT postures, and the posture decides what the stack should return: `join` GATES the join (it runs BEFORE the client becomes a member, so a denial means it never sees a fan-out) — return `{ allowed: c.bool(true) }` (optional `reason` reaches the client) or any truthy value to admit, and an EMPTY OR FALSY RETURN DENIES, so a stack that just falls through — or a gating trigger with NO `response` — refuses everyone, and a CRASH DENIES too. ONCE the object carries an `allowed` key admission needs STRICTLY `true` — a computed `1`/`"yes"` there DENIES. That is the inverse of a crashing message, which still delivers, and of `deliver` below. A lifecycle trigger's inputs are PINNED to those four, so a channel PATH PARAM is NOT among them — `inp("room_id")` RAISES, which crashes the gate and so REFUSES every client; take the param from `s.realtime.get_session` (`ref("session.params.room_id")`). A gate establishes NO auth, so `ref("auth.id")` reads 0 even when authenticated identity is `t.client("permissions.dbo_id")` or the session. A SERVER connect/disconnect has no channel, so no params at all; `leave` is OBSERVATIONAL (return ignored, throws swallowed); `deliver` GATES delivery PER RECIPIENT — the per-viewer redaction tool and the most expensive action here (a stack per recipient per message), and it needs `delivery.perRecipient` on the channel to run at all — BOTH HALVES are required, so a `deliver` trigger on a channel without the flag NEVER RUNS and every subscriber receives the UNREDACTED payload (no error, no log line); `export()` warns on each half alone. **`deliver`'s RETURN VALUES DO NOT READ LIKE A FILTER:** ONLY an explicit NULL drops the message for that recipient; an OBJECT replaces that recipient's payload; ANYTHING ELSE — INCLUDING `false`, `0`, `""` — DELIVERS IT UNCHANGED, as does a crash. So `return false` from a yes/no redaction check SENDS the message it was written to suppress — return null instead. The delivered payload arrives NESTED, so read `t.payload("<field>")`, and `t.client` is the SENDER while `s.realtime.get_session` describes the RECIPIENT this run is for.
778
777
  - `mcpServerTrigger({ name, mcpServer, stack?, response?, responseShape? })` / `agentTrigger({ name, agent, stack?, response?, responseShape? })` — toolset connection. Bind with the `mcpServer()`/`agent()` def handle (or its name) — it resolves to the toolset guid at export. Raw numeric `objId` is the escape hatch, rarely right: ids are assigned at import, so a handle passed to `objId` is a type error, and binding nothing deploys a trigger that never fires. Inputs: `t.toolset` (`t.toolset("name")`), `t.tools`. Response-bearing; the default stack copies `toolset`/`tools` into vars and returns them.
779
778
  - `workspaceTrigger({ name, actions?: {branch_live?,branch_merge?,branch_new?}, stack? })` — branch lifecycle. Inputs: `t.to_branch`, `t.from_branch`, `t.action`. Config-only.
780
779
  - `errorTrigger({ name, stack? })` — error-signature trigger. Inputs: `t.event` (`new|regression|fixed`), `t.id`, `t.signature`, `t.error` (`t.error("code")`/`t.error("message")`), `t.caller`, `t.statement`, `t.actor`, `t.count`, `t.first_seen`, `t.last_seen`, `t.fixed_at`. Config-only.
@@ -815,13 +814,14 @@ primary key `id`):
815
814
  - ⚠ `like`/`ilike` take the operand as the PATTERN, verbatim: a bare term matches only an exact whole-string equal, and the endpoint answers HTTP 200 with zero rows — nothing reports a problem, so a search box that matches nothing ships. For substring matching use `includes`/`not includes`, which wrap the operand in `%…%` themselves and match case-INSENSITIVELY. Prefer them over a hand-built `"%" + term + "%"`, which is non-empty even for an empty term and so defeats `ignoreEmpty`; `includes` composes with it. `contains`/`@>`/`overlaps` are JSON/array containment, not text — on a text column they 400 `ParseError: Invalid value for param`.
816
815
  - Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).
817
816
  - An operand may be a bare value (`col`/`inp`/`ref`/`auth`/`c.*`) OR a **filtered** value (`withFilters(...)`) inline — the engine compiles the string and arithmetic filters (`trim`, `concat`, `upper`, `lower`, …) into the SQL.
818
- - ⚠ The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
817
+ - ⚠ The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) — by raw name, since `fl.*` has no builder for them — or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
819
818
  - `bind: [{ table, as?, join?, where? }]` — joins (`context.bind[]`). `join` defaults to `"inner"`. `as` defaults to the table name; two joins to the same table need distinct aliases.
820
819
  - ⚠ In `where`/`sort`/`eval` a JOINED column takes a dotted path (`col("team_row.id")`); THIS query's own columns stay **bare** (`col("team")`). Qualifying your own by table name needs `tableAlias` (same rule as `aggregate`) — without it the engine reads the operand as text and 400s `ParseError: Invalid value for param` naming the OTHER operand, so it throws at export instead.
821
820
  - `bind: [{ table: team, as: "team_row", join: "left", where: expr(col("team"), "=", col("team_row.id")) }]`
821
+ - ⚠ A join does NOT put the joined table's columns on the returned row — with or without a `bind`, a row is the QUERIED table's columns, which is what `InferResponse` types. There is no `row.team_row`. To read a joined column, PROJECT it with an `eval` whose `name` is the dotted path: `eval: [{ name: "team_row.name", as: "team_name" }]` puts `team_name` on the row and on the inferred type. A bare `name` there is `Unsupported parameter reference` at runtime (it qualifies to the base table), and a dotted joined column in `output` is dropped with no error.
822
822
  - `returnType` — `"list"` (default) | `"single"` | `"count"` | `"exists"` | `"stream"` | `"aggregate"`. Drives `context.return.type` AND the `InferResponse` shape: `count`→`number`, `exists`→`boolean`, `single`→`Row|null`, `stream`→`Row[]` (pageable, no envelope), `list`→`Row[]`/envelope, `aggregate`→rows keyed by the `aggregate.group`/`eval` aliases. ⚠ A bare `count` of ZERO serializes as an EMPTY body, not `0` — a client parsing JSON gets a parse error on the one result it most needs to handle. Wrap it: `response: { count: ref("n") }`.
823
823
  - `eval: [{ name, as, filters? }]` — computed columns (`context.eval[]`). Each `as` grafts onto the row as an `unknown` key in `InferResponse`; shadowing a real column throws. Write `name` **bare** (`"embedding"`) — it is alias-qualified on emit exactly like `aggregate` (a bare eval name is `Unsupported param format` at runtime), and the statement declares the alias it used. An `as` alias is `sort`able in the SAME query.
824
- - An `eval`/`sort`/`where` filter pipeline compiles to **SQL**, so it resolves a DIFFERENT registry than `fl.*` (which runs in the request): the vector family, geo `distance`/`within`/`covers`, `search_rank`, the aggregators. Exported as `QUERY_EXPRESSION_FILTERS`/`VECTOR_FILTERS`.
824
+ - An `eval`/`sort`/`where` filter pipeline compiles to **SQL**, so it resolves a DIFFERENT registry than `fl.*` (which runs in the request): the vector family, geo `distance`/`within`/`covers`, `search_rank`, the aggregators. `fl.*` carries NO builder for these — reach them by raw name, `filter("epochms_add_day", …)` or the `{ name, arg }` form. The name lists are `QUERY_EXPRESSION_FILTERS`/`VECTOR_FILTERS` on `@xanots/sdk/internal`.
825
825
  - **Vector similarity search** — the ONLY way to query an `f.vector` column (no `SearchOp` does distance). `eval: [{ name: "embedding", as: "distance", filters: [{ name: "vector_cos_distance", arg: [inp("q")] }] }]` + `sort: [{ sortBy: "distance", dir: "asc" }]` ranks in the DATABASE over the column's index. Match the filter to the index `op` (`vector_cos_distance`↔`vector_cosine_ops`, `vector_l2_distance`↔`vector_l2_ops`, `vector_l1_distance`↔`vector_l1_ops`, `vector_inner_product`↔`vector_ip_ops`); `vector_cos_similarity` is the inverse, so sort it `desc`. The same filter on a `where` operand cuts off BY distance instead of by row count.
826
826
  - `aggregate: { group?, eval?, sort?, paging? }` (with `returnType:"aggregate"`) builds `context.return.aggregate`. `group`/`eval` are `{ name, as, filters? }`, an aggregator like `sum`/`count` riding `filters`. Some aggregators resolve ONLY here, not in a runtime value pipeline: `count_distinct`, `median`, `to_list`/`to_distinct_list` (each with `_asc`/`_desc`), and `vector_distance`.
827
827
  - ⚠ Write each `name` as a **bare** column (`"status"`). It is alias-qualified to `"<alias>.status"` on emit — the engine rejects an unqualified column in an aggregate with `Unsupported param format`. An already-dotted `name` (a `bind`ed/joined column) passes through.
@@ -939,7 +939,7 @@ Microservices (the `microservice()` def and the statement that calls it):
939
939
  - `c.array(a: Json[]) => Value` — Array constant (JSON string) → tag "const:array". Plain JSON literals only — a nested tagged value is rejected, same as c.obj.
940
940
  - `c.expression(source: string) => Value` — Xano Expression Engine source, passed through VERBATIM → tag "const:expr2". The string IS the expression: c.expression('"Hi, " ~ $input.name'), c.expression("$var.price * $var.qty"). ⚠️ NOT VALIDATED — never parsed or type-checked, invisible to InferResponse, and untouched by a rename that updates every typed ref(); a typo surfaces at runtime or as a wrong answer. Use it ONLY for syntax the typed surfaces cannot express (~ concatenation, inline arithmetic, conditionals) — prefer ref/inp/col, withFilters+fl.*, and obj() (which BUILDS a checked expression). Not the expr() condition builder.
941
941
  - `c.now() => Value` — Current time as epoch-ms — the engine-native const:epochms constant (no filter). Valid inline as a where/cmp operand. For cutoff math (cutoff = now - max_age) either compare inline or, for reuse/readability, hoist it into an s.set_var and compare against the var.
942
- - `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — reach for the bare form, it is shorter. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
942
+ - `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — prefer the bare form. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT that carries its own chain or is a c.now() (a trailing | binds to the whole value, not one argument, and c.now() needs one), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
943
943
  - `ref(name: string, opts?: { safe?: boolean }) => Value` — Reference a stack variable → tag "var". Pass { safe: true } for null-safe nested access — a dotted ref("owner.user_id", { safe: true }) compiles through the get filter so it resolves to null instead of raising "Unable to locate var" when the base is null.
944
944
  - `inp(name: string) => Value` — Reference a function/endpoint input → tag "input". Resolves ONLY against the `input` block of the def it sits in — a value produced earlier in the stack is `ref("var.field")`, not `inp("field")`. A name that is not declared here deploys clean and fails at runtime with ERROR_FATAL "Unable to locate input: <name>" on every branch that reads it; `export()` warns, and `--strict` fails the build. Sending the name in the request does NOT rescue it — an undeclared input is never bound, so the call fails identically with the value present. A dotted path drills INTO a declared input (`inp("action.amount")` needs a declared `action`).
945
945
  - `col(name: string) => Value` — Reference a table column → tag "col".
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # xanots v0.0.11
1
+ # xanots v0.0.13
2
2
 
3
3
  > TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
4
4
 
@@ -16,27 +16,26 @@ caller, `c.*` a constant — resolved at request time; JS operators over them do
16
16
  compute (see Gotchas). Requests share no memory — state persists in tables or redis.
17
17
 
18
18
  Coverage: object kinds 25/31, statement surfaces 214/214, filters 226 (226 typed).
19
- Not authorable here: branch, market_item, realtime_channel, run.job, run.service, tablemap — these cannot be authored and do not survive a pull; see `coverage.objectKinds.unmodeled` in `manifest.json`.
19
+ Not authorable here: branch, market_item, realtime_channel, run.job, run.service, tablemap — these cannot be authored and do not survive a pull; reasons in `coverage.objectKinds.unmodeled`.
20
20
 
21
21
  This file is the whole always-read surface: the mental model, the deploy contract,
22
22
  every gotcha, and control flow. Per-surface detail lives in the topic files listed
23
- below — open the one whose condition matches the task, and skip the rest. For
24
- exhaustive per-entry detail in NEITHER — a statement's full field schema with engine
25
- defaults, a filter's complete argument list, the engine `storedName` mapping — do a
26
- TARGETED lookup in the shipped `manifest.json` (a program imports it as
27
- `@xanots/sdk/manifest.json`, which Node ESM needs `with { type: "json" }` on;
28
- grep or `jq` the one entry you need;
29
- it is ~65k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
23
+ below — open the one whose condition matches the task, skip the rest. For
24
+ exhaustive per-entry detail in NEITHER — a statement's field schema with engine
25
+ defaults, a filter's full argument list, the `storedName` mapping — do a TARGETED
26
+ lookup in the shipped `manifest.json` (a program imports it as
27
+ `@xanots/sdk/manifest.json`, needing `with { type: "json" }` in Node ESM;
28
+ it is ~60k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
30
29
  jq '.statements[] | select(.sPath=="db.get")' manifest.json
31
30
  jq '.filters[] | select(.name=="json_decode")' manifest.json
31
+ ⚠ `fields` is null on the 65 `declarative: false` statements (`db.get`, …): typed wrappers whose arguments are the factory's `.d.ts` signature. Null ≠ missing.
32
32
  Select a statement on `sPath` (the `s.*` path you write), NOT `surface` (the XanoScript term): 24 of 214 differ — `var`→`set_var`, `break`→`foreach_break`, `foreach.remove`→`foreach_remove`, and every `expect.*`.
33
33
 
34
34
  ## Topic files
35
35
 
36
- Each line is a condition on the task. Open the files whose condition matches and
37
- skip the rest paths are relative to this file (`node_modules/@xanots/sdk/` once
38
- installed), so a plain file read resolves them at the version you have.
39
- `llms-full.txt` is everything concatenated — one fetch, for a reader that cannot open files.
36
+ Paths are relative to this file (`node_modules/@xanots/sdk/` once installed), so a
37
+ plain file read resolves them at the version you have.
38
+ `llms-full.txt` is everything concatenated one fetch for a reader that cannot open files.
40
39
 
41
40
  - [Object kinds](llms/object-kinds.md): Read when deciding WHAT to build — every authorable primitive (tables, endpoints, functions, tasks, agents, MCP, realtime, microservices, …), what each is, and which factory + register method build it. Read it too when a `register*` call will not typecheck because the array was built by `.flatMap()`/`.concat()` across modules.
42
41
  - [Core def shapes](llms/kinds-core.md): 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.
@@ -336,11 +335,11 @@ Non-obvious authoring rules:
336
335
  no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
337
336
  `getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
338
337
  the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
339
- ⚠ A FLOOR — **~289 kB minified (~57 kB gzipped)** for the FIRST def; splitting modules
340
- never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
341
- one, adds ~1 kB so reducing what a def does will not reduce it.
338
+ ⚠ A FLOOR — **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules
339
+ never removes it. The floor is the RUNTIME, not the def: a second or much richer def
340
+ adds ~2 kB, so trimming a def does not shrink it.
342
341
  Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
343
- plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
342
+ plain data importing NOTHING, still compile-checked: `routePath("GET blog/{slug}", { slug })`
344
343
  `channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
345
344
  URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
346
345
  - **Intra-workspace imports use `.js` specifiers** (`../tables/links.js`), not
@@ -417,7 +416,7 @@ Non-obvious authoring rules:
417
416
  submission on bind, so `s.security.check_password` compares two different hashes
418
417
  and a correct password always fails (`ok:false` on a found row). Take the submitted
419
418
  password as `input.text()` on both signup and login and pass the plaintext straight
420
- to `check_password` (which does the comparison hash itself).
419
+ to `check_password` (which does the comparison hash itself). `export()` warns.
421
420
  - **Agents authenticate with env vars — never `xanots login`.** `login` blocks on a
422
421
  browser consent no agent can complete. Set `$XANO_INSTANCE_URL` + `$XANO_WORKSPACE_ID`
423
422
  + `$XANO_META_TOKEN` and run `deploy`/`release` directly: no disk, no rotation, so it
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xanots",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.",
5
5
  "coverage": {
6
6
  "objectKinds": {
@@ -103,7 +103,7 @@
103
103
  {
104
104
  "name": "obj",
105
105
  "signature": "(fields: Record<string, Value | nested>) => ObjValue<typeof fields>",
106
- "description": "Dynamic object value → tag \"const:expr2\" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:\"a.b\"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref(\"row.id\") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref(\"row.id\") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — reach for the bare form, it is shorter. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args."
106
+ "description": "Dynamic object value → tag \"const:expr2\" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:\"a.b\"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref(\"row.id\") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref(\"row.id\") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — prefer the bare form. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT that carries its own chain or is a c.now() (a trailing | binds to the whole value, not one argument, and c.now() needs one), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args."
107
107
  },
108
108
  {
109
109
  "name": "ref",
@@ -9393,6 +9393,10 @@
9393
9393
  "flag": "--json",
9394
9394
  "description": "Force JSON on stdout (default: whenever stdout is not a terminal)"
9395
9395
  },
9396
+ {
9397
+ "flag": "--no-refresh",
9398
+ "description": "Do not refresh the managed block in CLAUDE.md / AGENTS.md / the Cursor rule"
9399
+ },
9396
9400
  {
9397
9401
  "flag": "--help, -h",
9398
9402
  "description": "Show help for the command and exit"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xanots/sdk",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "XanoTS — your Xano backend as TypeScript. `xanots deploy` ships your typed workspace (and an optional static frontend) to a live, auto-expiring ephemeral environment and prints its URL. init → deploy → URL.",
5
5
  "keywords": [
6
6
  "xanots",