@xano-sdk/chatbot 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +437 -0
- package/LICENSE +21 -0
- package/README.md +697 -0
- package/dist/index.d.ts +1811 -0
- package/dist/index.js +1128 -0
- package/llms.txt +487 -0
- package/package.json +88 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Instructions for coding agents (and humans) working **on** this repository.
|
|
4
|
+
For agents *consuming* the published package, see [llms.txt](llms.txt) instead.
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
`@xano-sdk/chatbot` ships a working conversational AI assistant as typed
|
|
9
|
+
[Xano SDK](https://www.npmjs.com/package/@xano/sdk) defs: two tables, a Xano AI
|
|
10
|
+
agent, one shared reply function, and two endpoint families. It exports plain def
|
|
11
|
+
objects; there is **no runtime** — the consumer's `Xano` instance registers and
|
|
12
|
+
encodes them. Everything is verified at the compiled-output level.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run build # tsup → dist/ (esm + d.ts)
|
|
18
|
+
npm run typecheck # tsc --noEmit
|
|
19
|
+
npm run lint # eslint .
|
|
20
|
+
npm test # tsc --noEmit && vitest run (type-level tests need the typecheck)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Run `npm run typecheck && npm run lint && npm test` before committing.
|
|
24
|
+
|
|
25
|
+
### Never widen a stack
|
|
26
|
+
|
|
27
|
+
A query's `stack` must be a literal tuple. Two things silently destroy it:
|
|
28
|
+
|
|
29
|
+
- a helper returning `Statement[]`, spread into the stack;
|
|
30
|
+
- a conditional spread — `...(cond ? [x] : [])` — even inline.
|
|
31
|
+
|
|
32
|
+
Either one collapses the tuple, so every `as`/`ref()` in that stack (including
|
|
33
|
+
ones declared *after* the spread) resolves to `unknown` and the query's response
|
|
34
|
+
infers as `StackTupleWidened`. **Nothing in this repo fails when that happens** —
|
|
35
|
+
the bundle stays byte-identical and every runtime test passes. It surfaces only in
|
|
36
|
+
a consumer's typecheck, as a response type that quietly became useless.
|
|
37
|
+
|
|
38
|
+
Use `statements(...)` from core for a helper, and two explicit `statements(...)`
|
|
39
|
+
branches for a conditional. `test/types.test.ts` → "no endpoint's response is
|
|
40
|
+
widened away" is the regression guard; it asserts the negative directly, because
|
|
41
|
+
the positive is invisible here. A conditional spread inside a statement *field*
|
|
42
|
+
(e.g. `db.add`'s `data`) is fine — no inference depends on it.
|
|
43
|
+
|
|
44
|
+
## Layout
|
|
45
|
+
|
|
46
|
+
- `src/options.ts` — the public option surface and `resolveOptions`, the single
|
|
47
|
+
validation gate. Every check lives there, before any def is built.
|
|
48
|
+
- `src/tables/*.ts`, `src/agent/*.ts`, `src/functions/*.ts`, `src/api/*.ts` — def
|
|
49
|
+
**factories**, one kind per module (the two endpoint families are one module
|
|
50
|
+
each; see below).
|
|
51
|
+
- `src/register.ts` — `createChatbot(opts)` builds everything;
|
|
52
|
+
`registerChatbot(xano, opts)` builds and registers, returning the same def set
|
|
53
|
+
with the instance on `.xano`. It must keep returning the defs: they are
|
|
54
|
+
factories, so that handle is a consumer's only route to the registered defs and
|
|
55
|
+
the only way a frontend can derive types without a runtime def import.
|
|
56
|
+
- `src/api/client-types.ts` — the per-endpoint request/response types consumers
|
|
57
|
+
import. Derived from the query handles via `ReturnType<typeof …Queries>`, which
|
|
58
|
+
is purely type-level, so nothing is built and nothing reaches a bundle.
|
|
59
|
+
- `src/index.ts` — the public surface.
|
|
60
|
+
- `test/*.test.ts` — encode-level fidelity assertions.
|
|
61
|
+
- `test/bundle.test.ts` + `test/fixtures/golden-bundle.json` — the byte-exact
|
|
62
|
+
bundle contract.
|
|
63
|
+
- `test/published-docs.test.ts` — the tarball contract.
|
|
64
|
+
- `scripts/regen-golden.ts` — regenerates the fixture (`npm run fixture:regen`).
|
|
65
|
+
|
|
66
|
+
## Facts verified against a live instance
|
|
67
|
+
|
|
68
|
+
These were established by deploying probes to an ephemeral, not read from
|
|
69
|
+
documentation. Several are load-bearing and **not obvious from the SDK's source**,
|
|
70
|
+
so re-verify before changing anything that depends on them.
|
|
71
|
+
|
|
72
|
+
### Agent tools work on ephemeral environments and instance workspaces
|
|
73
|
+
|
|
74
|
+
_Not yet re-probed on `@xano/sdk` 1.0.0._
|
|
75
|
+
|
|
76
|
+
Verified end-to-end: the chat agent calls tools through the ordinary `send`
|
|
77
|
+
endpoint and returns their values across both live ephemeral deploys
|
|
78
|
+
(`xanosdk deploy`) and instance workspaces. `s.tool.call` works as well.
|
|
79
|
+
|
|
80
|
+
(Note: An earlier ephemeral environment issue where toolsets were reported
|
|
81
|
+
missing or disabled has been resolved in the engine).
|
|
82
|
+
|
|
83
|
+
### A tool the model is not told about is never called
|
|
84
|
+
|
|
85
|
+
With tools attached but no mention of them in the system prompt, the model
|
|
86
|
+
answered "I do not know the secret word" and never called the tool — the default
|
|
87
|
+
prompt's "say so rather than guessing" steers it away. Asked by name ("call the
|
|
88
|
+
probe_secret_word tool") it called it immediately and returned the value.
|
|
89
|
+
|
|
90
|
+
Hence `TOOLS_SYSTEM_PROMPT`, appended by `resolveOptions` whenever `tools` are
|
|
91
|
+
configured, to the caller's own prompt as well as the default.
|
|
92
|
+
|
|
93
|
+
### Concurrent sends to one thread interleave (verified 2026-08-17)
|
|
94
|
+
|
|
95
|
+
Five parallel sends to one conversation produced the role pattern `uuuuuaaaaa`,
|
|
96
|
+
not `uauauauaua`: every user turn was written before any assistant turn, so each
|
|
97
|
+
model call read a history containing the other in-flight messages and replied to
|
|
98
|
+
several at once. Nothing was lost (10 rows, `created_at` non-decreasing) and no
|
|
99
|
+
guard failed — the send path simply has no per-thread lock, and cannot cheaply
|
|
100
|
+
have one across a slow model call.
|
|
101
|
+
|
|
102
|
+
Row ids were also NOT monotonic with `created_at` under that load, which is worth
|
|
103
|
+
remembering before writing any id-ordering assumption into a test or a stack.
|
|
104
|
+
|
|
105
|
+
Treated as a documented client-side responsibility (serialize sends per thread),
|
|
106
|
+
not a server fix. Revisit only if a cheap per-conversation lock appears.
|
|
107
|
+
|
|
108
|
+
### An empty string is rejected as a MISSING param (re-verified 2026-08-17)
|
|
109
|
+
|
|
110
|
+
`input.text({ required: true })` refuses `""` exactly as it refuses an absent
|
|
111
|
+
param — `400 ERROR_CODE_INPUT_ERROR`, `"Missing param: <name>"`, **before the
|
|
112
|
+
stack runs**. Checked on all three guest endpoints, query-string and JSON-body
|
|
113
|
+
forms alike.
|
|
114
|
+
|
|
115
|
+
This CONTRADICTS the earlier finding that `required: true` accepts an empty
|
|
116
|
+
string, which is why `capabilityGuard`'s first precondition exists. The
|
|
117
|
+
precondition is therefore currently unreachable. It is kept on purpose — see the
|
|
118
|
+
comment at `src/api/guest.ts`. Re-verify before acting on either result: this is
|
|
119
|
+
engine behaviour that has already changed once.
|
|
120
|
+
|
|
121
|
+
### The engine really does decode the `messages` template into LLM roles
|
|
122
|
+
|
|
123
|
+
Two otherwise-identical agents, one `prompt` and one `messages`, same payload:
|
|
124
|
+
|
|
125
|
+
| payload | `prompt` | `messages` |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| valid `[{role,content}]` | works, 110 input tokens | works, **89** |
|
|
128
|
+
| plain text, not an array | works | `ERROR_FATAL` |
|
|
129
|
+
| malformed JSON | works, echoes syntax back | `ERROR_FATAL` |
|
|
130
|
+
| `[]` | works | `ERROR_FATAL` |
|
|
131
|
+
| role outside the provider's set | works | `ERROR_FATAL` |
|
|
132
|
+
| `system` role in the array | — | works, and is honored |
|
|
133
|
+
| extra keys (`id`, `created_at`) | — | ignored |
|
|
134
|
+
|
|
135
|
+
The refusals are the proof — a literal string cannot be malformed — and the token
|
|
136
|
+
gap is the quantitative version: JSON punctuation and the `"role"`/`"content"`
|
|
137
|
+
keys never reach the model. **Do not "simplify" the agent to `prompt`.**
|
|
138
|
+
|
|
139
|
+
### An empty `content` does not error — it confabulates
|
|
140
|
+
|
|
141
|
+
A blank user turn produced a reply inventing an entire fictional prior exchange
|
|
142
|
+
about correcting the word "mispelled". This is why `content` carries `min:1` at
|
|
143
|
+
the column and a precondition in the reply function. A confabulated turn written
|
|
144
|
+
into the transcript poisons every later turn's context, and nothing surfaces it.
|
|
145
|
+
|
|
146
|
+
### `auth` on a query is binary, and `auth()` in a public stack raises
|
|
147
|
+
|
|
148
|
+
| endpoint | request | result |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `auth` set | no token | `401 Unauthorized` |
|
|
151
|
+
| public | no token | `auth()` → `ACCESS_DENIED` |
|
|
152
|
+
| public | **valid** token | `auth()` → `ACCESS_DENIED` |
|
|
153
|
+
|
|
154
|
+
Older SDK docs said `auth()` is `null` on a public endpoint; it is not — it raises
|
|
155
|
+
(the SDK now documents this).
|
|
156
|
+
This is the entire reason there are two endpoint families. **Do not merge them.**
|
|
157
|
+
|
|
158
|
+
### A tool with no per-tool `auth` is a PUBLIC stack, and fails in silence
|
|
159
|
+
|
|
160
|
+
The most expensive defect this package has shipped (live build log,
|
|
161
|
+
2026-08-31). Eight tools were passed the way every other SDK collection takes
|
|
162
|
+
handles — `tools: [saveNote]` — and everything downstream looked right: the
|
|
163
|
+
bundle carried all eight against the agent with resolved guids and
|
|
164
|
+
`enabled: true`, and the deploy succeeded. Then every tool threw
|
|
165
|
+
`ERROR_CODE_ACCESS_DENIED` on its first statement, the agent swallowed the
|
|
166
|
+
throw, and the model answered *"I saved a note that the office door code is
|
|
167
|
+
4821"* while nothing was written.
|
|
168
|
+
|
|
169
|
+
The cause is the row above: a bare handle encodes `auth: false`, a bare handle
|
|
170
|
+
is therefore a PUBLIC tool stack, and `auth("id")` in a public stack raises
|
|
171
|
+
rather than resolving to null.
|
|
172
|
+
|
|
173
|
+
What made it expensive is that the failure is invisible from every surface an
|
|
174
|
+
agent can reach — no 500, no error in the reply, no build or export warning, and
|
|
175
|
+
the model's confident sentence is itself misleading evidence. Finding the error
|
|
176
|
+
string took a temporary `debug_event` table, a probe tool wrapping `auth("id")`
|
|
177
|
+
in `s.try_catch`, a public read endpoint, and three deploy cycles.
|
|
178
|
+
|
|
179
|
+
`resolveOptions` now gives every tool entry that names no `auth` the configured
|
|
180
|
+
`authTable` (`src/options.ts` → `applyDefaultToolAuth`), so the bare form works.
|
|
181
|
+
`{ tool, auth: false }` is the explicit opt-out and the only spelling that still
|
|
182
|
+
produces a public tool. Enabling both endpoint families with scoped tools warns:
|
|
183
|
+
one agent serves both and an entry carries one `auth`, so a scoped tool has no
|
|
184
|
+
identity to bind on a public guest send.
|
|
185
|
+
|
|
186
|
+
**Verified on ephemeral `emed-ivuz-9b4e`, 2026-09-01** — `local-files/probe/`
|
|
187
|
+
reproduces the log's shape exactly (bare handles, `auth("id")` as each tool's
|
|
188
|
+
first statement). Through the ordinary `send` endpoint: "Save a note that the
|
|
189
|
+
office door code is 4821" now WRITES the row, with `user_id` bound to the
|
|
190
|
+
caller. A second user asking the same agent to list their notes gets `[]` and
|
|
191
|
+
the row of the first user is invisible to them, so the scoping is real and not
|
|
192
|
+
merely present in the bundle.
|
|
193
|
+
|
|
194
|
+
### Tool calls live in `steps[].content[]` — the top-level `toolCalls` is always empty
|
|
195
|
+
|
|
196
|
+
From the same log. `ChatReply.tool_calls` was null on every send, which reads as proof
|
|
197
|
+
that no tool ran — the worst possible answer during the debugging session above,
|
|
198
|
+
where every tool HAD run and thrown.
|
|
199
|
+
|
|
200
|
+
The response bound `ref("run.tool_calls")`, a key the agent-run envelope does not
|
|
201
|
+
carry. Core types the envelope as `AgentRunResult` — `result`, `finishReason`,
|
|
202
|
+
`steps`, `toolCalls`, `usage` — so `toolCalls` looked like the answer. It is not.
|
|
203
|
+
**Probed live (ephemeral `emed-ivuz-9b4e`, 2026-09-01): the top-level `toolCalls` is
|
|
204
|
+
present and always `[]`, even on a run whose tool wrote a row.** The calls are one
|
|
205
|
+
level down:
|
|
206
|
+
|
|
207
|
+
```jsonc
|
|
208
|
+
steps[0].content[0] = { "type": "tool-call", "toolCallId": "…", "toolName": "save_note",
|
|
209
|
+
"input": { "body": "…" } }
|
|
210
|
+
steps[0].content[1] = { "type": "tool-result", "toolCallId": "…", "toolName": "save_note",
|
|
211
|
+
"input": { … }, "output": { "id": 2, "user_id": 2, … } }
|
|
212
|
+
steps[1].content[0] = { "type": "text", "text": "OK. I've saved that." }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
So the reply function reads `run|get:"steps",[]` and loops: per step, `index_by("type")`
|
|
216
|
+
then `get("tool-call", [])` — both entry types carry `toolName`, so without the split
|
|
217
|
+
every call is reported twice — then maps to the name and `array_merge`s onto an
|
|
218
|
+
accumulator. Names only: a tool-result carries the tool's whole return value, which is
|
|
219
|
+
data no client asked for.
|
|
220
|
+
|
|
221
|
+
**`fl.map` with a JS lambda does the same job in one filter, and was verified to work.**
|
|
222
|
+
It is not used: core is explicit that a lambda is an escape hatch drawing on a bounded
|
|
223
|
+
workspace-wide worker pool, and every send in every install would pay for work four
|
|
224
|
+
ordinary statements express. `fl.flatten()` is not an option either — it flattens all
|
|
225
|
+
the way to scalar VALUES, so a list of step-content objects comes back as 15 strings.
|
|
226
|
+
|
|
227
|
+
Verified end to end through the real `send` endpoint: two `save_note` calls and a
|
|
228
|
+
`list_notes` in one turn returned `["save_note","save_note","list_notes"]` — order kept,
|
|
229
|
+
duplicates kept — and a turn the model answered directly returned `[]`.
|
|
230
|
+
|
|
231
|
+
### Route names are not verb pairs
|
|
232
|
+
|
|
233
|
+
The SDK composes a query's identity from `(api group, verb, name)`, and
|
|
234
|
+
`xanosdk routes --emit` keys its manifest on `"<VERB> <name>"`, so `GET foo` and
|
|
235
|
+
`POST foo` do not collide. The route names stay distinct and `routePrefix` stays
|
|
236
|
+
anyway: a query's identity includes its name, so a rename moves every endpoint's
|
|
237
|
+
identity and every consumer's `xano.lock` with it.
|
|
238
|
+
|
|
239
|
+
⚠ **The dev pin matters.** The golden fixture is generated against
|
|
240
|
+
`devDependencies`, and query guids derive from the SDK's identity rules. Bumping
|
|
241
|
+
the pin is a reviewed act with a real fixture diff behind it — not drift to
|
|
242
|
+
regenerate past.
|
|
243
|
+
|
|
244
|
+
### Reproducing any of this
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
xanosdk deploy ./xano/index.ts --name probe --expires-hours 2
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
If `deploy` reports `must be owner of table mvpw1_N` it is refusing to re-import
|
|
251
|
+
over an existing ephemeral; run `xanosdk ephemeral delete probe` (or delete
|
|
252
|
+
`.xano/ephemeral.json`) and deploy again to get a fresh one.
|
|
253
|
+
|
|
254
|
+
## Rules that bite
|
|
255
|
+
|
|
256
|
+
- **The defs are FACTORIES, not module singletons.** `f.tableRef` resolves its
|
|
257
|
+
target's guid eagerly at column-construction time, so a module-level
|
|
258
|
+
`conversation` def would bake in one auth table forever. This is the one
|
|
259
|
+
structural divergence from `@xano-sdk/auth`, and everything else follows from it —
|
|
260
|
+
including the *absence* of auth's ~100 lines of canonical/history conflict
|
|
261
|
+
guards, which exist only because its group is a process-wide singleton. Do not
|
|
262
|
+
"restore symmetry" with auth by hoisting a def to module scope.
|
|
263
|
+
- **`registerChatbot` still needs its WeakSet.** For a different reason than
|
|
264
|
+
auth's: core's duplicate-def guard compares def *identity*, and two
|
|
265
|
+
`createChatbot` calls produce distinct objects sharing names, so the mistake
|
|
266
|
+
slips past core and lands at `export()` as `Duplicate object guid … shared by
|
|
267
|
+
"dbo/conversation" and "dbo/conversation"` — naming neither call.
|
|
268
|
+
- **Nothing may import `@xano-sdk/auth`.** Not as a dependency, not as a
|
|
269
|
+
peer dependency, not in an example that would make it load-bearing. `authTable`
|
|
270
|
+
is the whole integration surface. A test that needs an auth table declares one.
|
|
271
|
+
- **References use def handles, never bare names**, inside the package. The
|
|
272
|
+
*consumer* may pass a bare name for `authTable`; everything this package
|
|
273
|
+
references itself (the conversation table from the message table, the reply
|
|
274
|
+
function from the send endpoints) uses the handle.
|
|
275
|
+
- **Pin no guids, and no canonical by default.** Identity belongs to the
|
|
276
|
+
consumer's `xano.lock` (fallback: `md5("<kind>:<name>")`; queries seed on
|
|
277
|
+
group + verb + name). The one exception is
|
|
278
|
+
the opt-in `{ canonical }`, which the consumer supplies.
|
|
279
|
+
- **Security defaults are not preferences.** Request history off; `session_token`
|
|
280
|
+
`internal` and uniquely indexed; the blank-token precondition; `notfound` rather
|
|
281
|
+
than `accessdenied` on someone else's thread; the unclaimed check on both the
|
|
282
|
+
guest guard and the claim endpoint. Each has a comment naming the failure it
|
|
283
|
+
prevents. Changing one needs a stated reason, not a tidier-looking stack.
|
|
284
|
+
- **The authenticated create mints a `session_token` when guests are on.** Not
|
|
285
|
+
redundant — an omitted column takes its type default (`""`) on `db.add`, so the
|
|
286
|
+
*second* authenticated create used to die on the unique index, and every
|
|
287
|
+
authenticated row sharing `""` made an empty guest token match a logged-in
|
|
288
|
+
user's thread. Caught live; do not remove it as dead code.
|
|
289
|
+
- **`DEFAULT_SYSTEM_PROMPT` asks for light markdown on purpose.** It is not
|
|
290
|
+
filler: the README tells frontends to render `reply` as markdown, and that only
|
|
291
|
+
works if the model is told to produce it — the two are halves of one decision, so
|
|
292
|
+
do not drop the clause from one side alone. "Light" is also deliberate; a model
|
|
293
|
+
told plainly to "use markdown" reaches for headings and tables in a two-line
|
|
294
|
+
answer, which reads worse in a chat bubble than prose. The `ChatReply.reply`
|
|
295
|
+
doc and README both carry the matching warning that a rendered reply is
|
|
296
|
+
model output shaped by user input, so the renderer must keep raw HTML off.
|
|
297
|
+
- **`llm.prompt` / `llm.messages` are refused**, in the type and at runtime. The
|
|
298
|
+
package owns the run prompt because that is how the transcript is delivered, and
|
|
299
|
+
Xano stores one prompt behind a `prompt_type` discriminator, so a supplied one
|
|
300
|
+
would replace the history rather than add to it.
|
|
301
|
+
- **Values stay explicit `c.*`, never bare literals**, matching `@xano-sdk/auth`.
|
|
302
|
+
Core coerces raw literals inside some maps; keeping the tag is load-bearing
|
|
303
|
+
where a constant is a magic string the engine interprets (`c.text("now")`).
|
|
304
|
+
- **The two endpoint families live one module each**, unlike auth's
|
|
305
|
+
one-def-per-module layout. Five separate modules here would be five copies of an
|
|
306
|
+
identical five-parameter factory signature; the shared authorization idiom
|
|
307
|
+
(`ownershipGuard` / `capabilityGuard`) is what justifies the grouping.
|
|
308
|
+
|
|
309
|
+
## Auto-wiring
|
|
310
|
+
|
|
311
|
+
`package.json` carries a `"xanosdk"` block (`register: registerChatbot`, `returns: handle`,
|
|
312
|
+
`options.authTable` → `@xano-sdk/auth`'s `userTable`) that `xanosdk init --marketplace`
|
|
313
|
+
reads to write the registration into `xano/index.ts`. The SDK offers it only when
|
|
314
|
+
`@xano-sdk/auth` is also installed; otherwise it prints the call and leaves it unwired.
|
|
315
|
+
`test/manifest.test.ts` pins the block against the real export.
|
|
316
|
+
|
|
317
|
+
## The peer range
|
|
318
|
+
|
|
319
|
+
`@xano/sdk` is a `peerDependency` with the window `>=<floor> <2.0.0` — the floor is
|
|
320
|
+
currently `1.0.0` and the ceiling is the next major. `devDependencies` carries the
|
|
321
|
+
version actually tested, pinned **exactly** (no caret) because it is the single
|
|
322
|
+
version the golden fixture was generated against.
|
|
323
|
+
|
|
324
|
+
Verify the floor by installing it and running the suite, rather than copying the
|
|
325
|
+
number forward:
|
|
326
|
+
|
|
327
|
+
- `security.create_uuid` is the uuid helper; `create_guid` was removed and must
|
|
328
|
+
not come back.
|
|
329
|
+
- The reply function's `responseShape: {} as ChatReply` is **load-bearing**.
|
|
330
|
+
Without it, the `function.run` brand in each send endpoint carries the reply
|
|
331
|
+
function's whole derived response type, which is too deep for tsup's dts emit
|
|
332
|
+
(a location-less TS2589 that fails `npm run build`) and for a consumer's
|
|
333
|
+
`bot.queries.map((q) => …)`. Plain `tsc --noEmit` does not catch the build
|
|
334
|
+
half — run `npm run build` on every bump.
|
|
335
|
+
|
|
336
|
+
Do not raise the floor without a reason: needlessly raising it forces consumers
|
|
337
|
+
into an upgrade that buys them nothing. `test/helpers.ts` deliberately computes
|
|
338
|
+
`md5("<kind>:<name>")` itself rather than importing `deriveGuid` from
|
|
339
|
+
`@xano/sdk/internal` — importing it would raise the floor for every consumer to
|
|
340
|
+
satisfy a test.
|
|
341
|
+
|
|
342
|
+
## Versions
|
|
343
|
+
|
|
344
|
+
Versions start at 1.0.0 under `@xano-sdk/chatbot` and only 1.0.x increments for
|
|
345
|
+
now, regardless of the change. Do not bump unless told to.
|
|
346
|
+
|
|
347
|
+
## The golden-bundle contract
|
|
348
|
+
|
|
349
|
+
`test/bundle.test.ts` registers everything on a fresh `Xano`, calls `export()`,
|
|
350
|
+
and deep-equals the result against `test/fixtures/golden-bundle.json` (raw, no
|
|
351
|
+
normalizer). This is the peer-drift tripwire.
|
|
352
|
+
|
|
353
|
+
The golden config turns **everything** on — both families, a pinned canonical, a
|
|
354
|
+
keyed provider — because a tripwire only guards what it encodes. `bundle.test.ts`
|
|
355
|
+
has a second block asserting that coverage, so the config cannot quietly narrow.
|
|
356
|
+
|
|
357
|
+
Regenerating is a deliberate, reviewed act — never a way to make a red test pass.
|
|
358
|
+
A failure means the encoded bundle moved; find out *why* first.
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
npm run fixture:regen && git diff test/fixtures/golden-bundle.json
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Review that diff line by line (watch guids, auth flags, stack order, output lists,
|
|
365
|
+
`prompt_type`) before committing.
|
|
366
|
+
|
|
367
|
+
## Release
|
|
368
|
+
|
|
369
|
+
Lockstep with the peer. For each SDK bump:
|
|
370
|
+
|
|
371
|
+
1. Read the SDK's `CHANGELOG.md` entries between the two versions, then its
|
|
372
|
+
`llms.txt` diff, before touching anything — a green suite proves no encoding drift, not that the
|
|
373
|
+
package still follows current guidance.
|
|
374
|
+
2. Move the `devDependencies` pin. Move the `peerDependencies` floor **only** if a
|
|
375
|
+
new core behaviour or type became load-bearing here, and *verify* it by
|
|
376
|
+
installing that version and running the suite.
|
|
377
|
+
3. Run `npm run typecheck && npm run lint && npm test`. Regenerate the fixture
|
|
378
|
+
only if the bundle legitimately changed. An unchanged fixture is the expected
|
|
379
|
+
outcome of most bumps, not a reason to look harder.
|
|
380
|
+
4. Re-run the live probes if anything touched the agent, the reply function, or an
|
|
381
|
+
authorization guard. The encode-level suite cannot catch a change in what the
|
|
382
|
+
*engine* does with the bytes.
|
|
383
|
+
5. Update the install notes in `README.md` **and** `llms.txt` with both numbers.
|
|
384
|
+
6. Ship it. Only 1.0.x increments for now, and only when told to bump.
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
npm run release:beta # prerelease: bumps, tags, publishes under `beta`
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
The stable path does **not** bump for you — `npm run release` only publishes.
|
|
391
|
+
Bump in the PR that carries the change, so the version reviewers approve is
|
|
392
|
+
the version that ships:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
npm version patch --no-git-tag-version # in the PR
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Then, after the PR merges, from a green tree on the default branch:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
git tag "v$(node -p 'require("./package.json").version')"
|
|
402
|
+
npm run release
|
|
403
|
+
git push --tags
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`npm pack --dry-run` should show exactly 7 files (`dist/` ×2 — no source map,
|
|
407
|
+
`README.md`, `AGENTS.md`, `llms.txt`, `LICENSE`, `package.json`).
|
|
408
|
+
`test/published-docs.test.ts` pins that list and checks every relative link in
|
|
409
|
+
a shipped doc resolves inside the tarball.
|
|
410
|
+
|
|
411
|
+
### Release notes
|
|
412
|
+
|
|
413
|
+
Start from [.github/RELEASE_TEMPLATE.md](https://github.com/xanots/chatbot/blob/main/.github/RELEASE_TEMPLATE.md) — it carries
|
|
414
|
+
the shape and the constraints the Slack announcement imposes, and its guidance
|
|
415
|
+
lives in HTML comments stripped before Slack sees them.
|
|
416
|
+
|
|
417
|
+
- The GitHub release **name** (not the tag) becomes the Slack header verbatim:
|
|
418
|
+
`vX.Y.Z — Three-to-five word theme`.
|
|
419
|
+
- Everything before the first `##` is the summary block. No story — a brief
|
|
420
|
+
paragraph and the install snippet.
|
|
421
|
+
- After the summary, one itemized title and short description per change. Anything
|
|
422
|
+
a consumer must act on (a breaking type, a peer-range move, a migration) gets
|
|
423
|
+
called out there too, not left for the reader to infer.
|
|
424
|
+
- **Each change gets its own `##` heading**, because those become the itemized
|
|
425
|
+
Slack bullets (first 8 shown). Write each as a claim that survives with no body
|
|
426
|
+
text under it. Purely structural headings (Notes, Compatibility, …) are dropped
|
|
427
|
+
from the bullets, so use them freely — just never hide a change under one.
|
|
428
|
+
|
|
429
|
+
Publishing a release fires `.github/workflows/release-slack.yml`, which runs
|
|
430
|
+
`.github/scripts/test_slack_release_message.py` in the same job that posts, so a
|
|
431
|
+
malformed payload fails the workflow rather than reaching Slack. Both the builder
|
|
432
|
+
and that test are kept identical to `xanots/sdk`'s and `xanots/auth`'s modulo the
|
|
433
|
+
repo and package names; port fixes between them rather than letting them diverge.
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
cd .github/scripts && python3 test_slack_release_message.py
|
|
437
|
+
```
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xano, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|