@decocms/bindings 0.1.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/README.md ADDED
@@ -0,0 +1,680 @@
1
+ # @decocms/bindings
2
+
3
+ Core type definitions and utilities for the bindings system. Bindings define standardized interfaces that integrations (MCPs - Model Context Protocols) can implement, similar to TypeScript interfaces but with runtime validation.
4
+
5
+ ## Installation
6
+
7
+ Install the package using npm or bun:
8
+
9
+ ```bash
10
+ npm install @decocms/bindings
11
+ ```
12
+
13
+ or with bun:
14
+
15
+ ```bash
16
+ bun add @decocms/bindings
17
+ ```
18
+
19
+ **Note:** This package requires `zod` as a peer dependency. If you don't already have `zod` installed in your project, you'll need to install it separately.
20
+
21
+ ## What are Bindings?
22
+
23
+ Bindings are a core concept for defining and enforcing standardized interfaces that MCPs can implement. They provide a type-safe, declarative way to specify what tools (methods) and schemas an integration must expose to be compatible with certain parts of the system.
24
+
25
+ ### Key Features
26
+
27
+ - **Standardization**: Define contracts (schemas and method names) that MCPs must implement
28
+ - **Type Safety**: Leverage Zod schemas and TypeScript types for correct data structures
29
+ - **Runtime Validation**: Check if an integration implements a binding at runtime
30
+ - **Extensibility**: Create new bindings for any use case
31
+
32
+ ## Usage
33
+
34
+ ### 1. Defining a Binding
35
+
36
+ A binding is an array of tool definitions, each specifying a name and input/output schemas:
37
+
38
+ ```typescript
39
+ import { z } from "zod";
40
+ import type { Binder } from "@decocms/bindings";
41
+
42
+ // Define input/output schemas
43
+ const joinChannelInput = z.object({
44
+ workspace: z.string(),
45
+ discriminator: z.string(),
46
+ agentId: z.string(),
47
+ });
48
+
49
+ const channelOutput = z.object({
50
+ success: z.boolean(),
51
+ channelId: z.string(),
52
+ });
53
+
54
+ // Define the binding
55
+ export const CHANNEL_BINDING = [
56
+ {
57
+ name: "DECO_CHAT_CHANNELS_JOIN" as const,
58
+ inputSchema: joinChannelInput,
59
+ outputSchema: channelOutput,
60
+ },
61
+ {
62
+ name: "DECO_CHAT_CHANNELS_LEAVE" as const,
63
+ inputSchema: z.object({ channelId: z.string() }),
64
+ outputSchema: z.object({ success: z.boolean() }),
65
+ },
66
+ {
67
+ name: "DECO_CHAT_CHANNELS_LIST" as const,
68
+ inputSchema: z.object({}),
69
+ outputSchema: z.object({
70
+ channels: z.array(z.object({
71
+ label: z.string(),
72
+ value: z.string(),
73
+ })),
74
+ }),
75
+ opt: true, // This tool is optional
76
+ },
77
+ ] as const satisfies Binder;
78
+ ```
79
+
80
+ ### 2. Checking if Tools Implement a Binding
81
+
82
+ Use `createBindingChecker` to verify if a set of tools implements a binding:
83
+
84
+ ```typescript
85
+ import { createBindingChecker } from "@decocms/bindings";
86
+
87
+ // Create a checker for your binding
88
+ const channelChecker = createBindingChecker(CHANNEL_BINDING);
89
+
90
+ // Check if available tools implement the binding
91
+ const availableTools = [
92
+ { name: "DECO_CHAT_CHANNELS_JOIN" },
93
+ { name: "DECO_CHAT_CHANNELS_LEAVE" },
94
+ { name: "DECO_CHAT_CHANNELS_LIST" },
95
+ { name: "OTHER_TOOL" },
96
+ ];
97
+
98
+ const isImplemented = channelChecker.isImplementedBy(availableTools);
99
+ console.log(isImplemented); // true - all required tools are present
100
+
101
+ // Optional tools don't need to be present
102
+ const minimalTools = [
103
+ { name: "DECO_CHAT_CHANNELS_JOIN" },
104
+ { name: "DECO_CHAT_CHANNELS_LEAVE" },
105
+ ];
106
+
107
+ const stillValid = channelChecker.isImplementedBy(minimalTools);
108
+ console.log(stillValid); // true - CHANNELS_LIST is optional
109
+ ```
110
+
111
+ ### 3. Using RegExp for Tool Names
112
+
113
+ You can use RegExp patterns for flexible tool matching:
114
+
115
+ ```typescript
116
+ export const RESOURCE_BINDING = [
117
+ {
118
+ name: /^DECO_RESOURCE_\w+_SEARCH$/ as RegExp,
119
+ inputSchema: z.object({ term: z.string() }),
120
+ outputSchema: z.object({ items: z.array(z.any()) }),
121
+ },
122
+ ] as const satisfies Binder;
123
+
124
+ // This will match: DECO_RESOURCE_WORKFLOW_SEARCH, DECO_RESOURCE_USER_SEARCH, etc.
125
+ ```
126
+
127
+ ### 4. Type Safety with Bindings
128
+
129
+ TypeScript can infer types from your binding definitions:
130
+
131
+ ```typescript
132
+ import type { ToolBinder } from "@decocms/bindings";
133
+
134
+ // Extract the type of a specific tool
135
+ type JoinChannelTool = typeof CHANNEL_BINDING[0];
136
+
137
+ // Get input type
138
+ type JoinChannelInput = z.infer<JoinChannelTool["inputSchema"]>;
139
+
140
+ // Get output type
141
+ type JoinChannelOutput = z.infer<NonNullable<JoinChannelTool["outputSchema"]>>;
142
+ ```
143
+
144
+ ## API Reference
145
+
146
+ ### Types
147
+
148
+ #### `ToolBinder<TName, TInput, TReturn>`
149
+
150
+ Defines a single tool within a binding.
151
+
152
+ - `name`: Tool name (string or RegExp)
153
+ - `inputSchema`: Zod schema for input validation
154
+ - `outputSchema?`: Optional Zod schema for output validation
155
+ - `opt?`: If true, tool is optional in the binding
156
+
157
+ #### `Binder<TDefinition>`
158
+
159
+ Represents a collection of tool definitions that form a binding.
160
+
161
+ #### `BindingChecker`
162
+
163
+ Interface with an `isImplementedBy` method for checking binding implementations.
164
+
165
+ ### Functions
166
+
167
+ #### `createBindingChecker<TDefinition>(binderTools: TDefinition): BindingChecker`
168
+
169
+ Creates a binding checker that can verify if a set of tools implements the binding.
170
+
171
+ **Parameters:**
172
+ - `binderTools`: The binding definition to check against
173
+
174
+ **Returns:**
175
+ - A `BindingChecker` with an `isImplementedBy` method
176
+
177
+ **Example:**
178
+ ```typescript
179
+ const checker = createBindingChecker(MY_BINDING);
180
+ const isValid = checker.isImplementedBy(availableTools);
181
+ ```
182
+
183
+ ## Common Patterns
184
+
185
+ ### Well-Known Bindings
186
+
187
+ The package includes pre-defined bindings for common use cases. Well-known bindings are organized in the `well-known` folder and must be imported directly:
188
+
189
+ - **Collections**: `@decocms/bindings/well-known/collections` - Collection bindings for SQL table-like structures
190
+ - **Models**: `@decocms/bindings/well-known/models` - AI model providers interface
191
+
192
+ See the [Collection Bindings](#collection-bindings) and [Models Bindings](#models-bindings) sections below for detailed usage examples.
193
+
194
+ ### Generic Bindings
195
+
196
+ Create factory functions for generic bindings:
197
+
198
+ ```typescript
199
+ function createResourceBinding(resourceName: string) {
200
+ return [
201
+ {
202
+ name: `DECO_RESOURCE_${resourceName.toUpperCase()}_SEARCH` as const,
203
+ inputSchema: z.object({ term: z.string() }),
204
+ outputSchema: z.object({ items: z.array(z.any()) }),
205
+ },
206
+ {
207
+ name: `DECO_RESOURCE_${resourceName.toUpperCase()}_READ` as const,
208
+ inputSchema: z.object({ uri: z.string() }),
209
+ outputSchema: z.object({ data: z.any() }),
210
+ },
211
+ ] as const satisfies Binder;
212
+ }
213
+
214
+ const workflowBinding = createResourceBinding("workflow");
215
+ ```
216
+
217
+ ## Collection Bindings
218
+
219
+ Collection bindings provide standardized CRUD + Search operations for SQL table-like collections, compatible with TanStack DB query-collection. They are designed for database tables where each entity has a unique URI and audit trail fields.
220
+
221
+ ### Key Features
222
+
223
+ - **SQL Table-like Structure**: Represents database tables with standardized operations
224
+ - **URI-based Identification**: Uses collection URIs in format `rsc://{connectionid}/{collection_name}/{item_id}`
225
+ - **Audit Trail**: All entities must include `created_at`, `updated_at`, `created_by`, and `updated_by` fields
226
+ - **TanStack DB Compatible**: Works seamlessly with TanStack DB's query-collection LoadSubsetOptions
227
+ - **Type-Safe**: Full TypeScript support with Zod validation
228
+
229
+ ### Base Entity Schema Requirements
230
+
231
+ All collection entity schemas must extend `BaseCollectionEntitySchema`, which requires:
232
+
233
+ - `uri`: Collection URI in format `rsc://{connectionid}/{collection_name}/{item_id}`
234
+ - `created_at`: Creation timestamp (datetime string)
235
+ - `updated_at`: Last update timestamp (datetime string)
236
+ - `created_by`: User who created the entity (optional string)
237
+ - `updated_by`: User who last updated the entity (optional string)
238
+
239
+ ### Using Collection Bindings
240
+
241
+ Collection bindings are a well-known binding pattern for SQL table-like structures. Import them from the well-known collections module:
242
+
243
+ ```typescript
244
+ import { z } from "zod";
245
+ import { createCollectionBindings } from "@decocms/bindings/well-known/collections";
246
+ import { createBindingChecker } from "@decocms/bindings";
247
+
248
+ // Define your entity schema extending the base schema
249
+ const TodoSchema = z.object({
250
+ uri: z.string(), // Collection URI: rsc://{connectionid}/todos/{item_id}
251
+ created_at: z.string().datetime(),
252
+ updated_at: z.string().datetime(),
253
+ created_by: z.string().optional(),
254
+ updated_by: z.string().optional(),
255
+ // Your custom fields
256
+ title: z.string(),
257
+ completed: z.boolean(),
258
+ userId: z.number(),
259
+ });
260
+
261
+ // Create the collection binding (full CRUD)
262
+ const TODO_COLLECTION_BINDING = createCollectionBindings("todos", TodoSchema);
263
+
264
+ // Create a read-only collection binding (only LIST and GET)
265
+ const READONLY_COLLECTION_BINDING = createCollectionBindings("products", ProductSchema, {
266
+ readOnly: true,
267
+ });
268
+
269
+ // Create a checker to verify if tools implement the binding
270
+ const todoChecker = createBindingChecker(TODO_COLLECTION_BINDING);
271
+
272
+ // Check if available tools implement the binding
273
+ const availableTools = [
274
+ { name: "DECO_COLLECTION_TODOS_LIST" },
275
+ { name: "DECO_COLLECTION_TODOS_GET" },
276
+ { name: "DECO_COLLECTION_TODOS_INSERT" },
277
+ { name: "DECO_COLLECTION_TODOS_UPDATE" },
278
+ { name: "DECO_COLLECTION_TODOS_DELETE" },
279
+ ];
280
+
281
+ const isImplemented = await todoChecker.isImplementedBy(availableTools);
282
+ console.log(isImplemented); // true if all required tools are present
283
+ ```
284
+
285
+ ### Collection Operations
286
+
287
+ The `createCollectionBindings` function generates tool bindings based on the `readOnly` option:
288
+
289
+ **Required operations (always included):**
290
+
291
+ 1. **LIST** - `DECO_COLLECTION_{NAME}_LIST`
292
+ - Query/search entities with filtering, sorting, and pagination
293
+ - Input: `where?`, `orderBy?`, `limit?`, `offset?`
294
+ - Output: `items[]`, `totalCount?`, `hasMore?`
295
+
296
+ 2. **GET** - `DECO_COLLECTION_{NAME}_GET`
297
+ - Get a single entity by URI
298
+ - Input: `uri`
299
+ - Output: `item | null`
300
+
301
+ **Optional operations (excluded if `readOnly: true`):**
302
+
303
+ 3. **INSERT** - `DECO_COLLECTION_{NAME}_INSERT`
304
+ - Create a new entity
305
+ - Input: `data` (without uri, which is auto-generated)
306
+ - Output: `item` (with generated URI)
307
+
308
+ 4. **UPDATE** - `DECO_COLLECTION_{NAME}_UPDATE`
309
+ - Update an existing entity
310
+ - Input: `uri`, `data` (partial)
311
+ - Output: `item`
312
+
313
+ 5. **DELETE** - `DECO_COLLECTION_{NAME}_DELETE`
314
+ - Delete an entity
315
+ - Input: `uri`
316
+ - Output: `success`, `uri`
317
+
318
+ ### Read-Only Collections
319
+
320
+ To create a read-only collection (only LIST and GET operations), pass `{ readOnly: true }` as the third parameter:
321
+
322
+ ```typescript
323
+ // Read-only collection - only LIST and GET operations
324
+ const READONLY_COLLECTION_BINDING = createCollectionBindings(
325
+ "products",
326
+ ProductSchema,
327
+ { readOnly: true }
328
+ );
329
+ ```
330
+
331
+ This is useful for collections that are managed externally or should not be modified through the MCP interface.
332
+
333
+ ## Models Bindings
334
+
335
+ Models bindings provide a well-known interface for AI model providers. They use collection bindings under the hood for LIST and GET operations, and add a streaming endpoint tool.
336
+
337
+ ### Using Models Bindings
338
+
339
+ ```typescript
340
+ import { MODELS_BINDING, MODELS_COLLECTION_BINDING } from "@decocms/bindings/well-known/models";
341
+ import { createBindingChecker } from "@decocms/bindings";
342
+
343
+ // Use the pre-defined MODELS_BINDING
344
+ const modelsChecker = createBindingChecker(MODELS_BINDING);
345
+
346
+ // Check if available tools implement the binding
347
+ const availableTools = [
348
+ { name: "DECO_COLLECTION_MODELS_LIST" },
349
+ { name: "DECO_COLLECTION_MODELS_GET" },
350
+ { name: "GET_STREAM_ENDPOINT" },
351
+ ];
352
+
353
+ const isImplemented = await modelsChecker.isImplementedBy(availableTools);
354
+ console.log(isImplemented); // true if all required tools are present
355
+ ```
356
+
357
+ ### Models Binding Tools
358
+
359
+ The `MODELS_BINDING` includes:
360
+
361
+ 1. **DECO_COLLECTION_MODELS_LIST** (required)
362
+ - List available AI models with their capabilities
363
+ - Uses collection binding LIST operation
364
+ - Input: `where?`, `orderBy?`, `limit?`, `offset?`
365
+ - Output: `items[]` (array of model entities)
366
+
367
+ 2. **DECO_COLLECTION_MODELS_GET** (required)
368
+ - Get a single model by URI
369
+ - Uses collection binding GET operation
370
+ - Input: `uri`
371
+ - Output: `item | null`
372
+
373
+ 3. **GET_STREAM_ENDPOINT** (required)
374
+ - Get the streaming endpoint URL for chat completions
375
+ - Input: `{}` (empty object, passthrough)
376
+ - Output: `{ url?: string }` (optional URL)
377
+
378
+ ### Model Entity Schema
379
+
380
+ Models follow the collection entity schema with additional model-specific fields:
381
+
382
+ ```typescript
383
+ {
384
+ uri: string; // Collection URI
385
+ created_at: string; // Creation timestamp
386
+ updated_at: string; // Last update timestamp
387
+ created_by?: string; // User who created
388
+ updated_by?: string; // User who last updated
389
+ id: string; // Model ID
390
+ model: string; // Model identifier
391
+ name: string; // Display name
392
+ logo: string | null; // Logo URL
393
+ capabilities: string[]; // Array of capabilities
394
+ contextWindow: number | null; // Context window size
395
+ inputCost: number | null; // Input cost per token
396
+ outputCost: number | null; // Output cost per token
397
+ outputLimit: number | null; // Output limit
398
+ description: string | null; // Model description
399
+ }
400
+ ```
401
+
402
+ ### MCP Implementation Example
403
+
404
+ Here's how you would implement the models binding in an MCP server:
405
+
406
+ ```typescript
407
+ import { MODELS_BINDING } from "@decocms/bindings/well-known/models";
408
+ import { parseCollectionUri } from "@decocms/bindings/well-known/collections";
409
+ import { impl } from "@decocms/sdk/mcp/bindings/binder";
410
+
411
+ const modelTools = impl(MODELS_BINDING, [
412
+ {
413
+ description: "List available AI models",
414
+ handler: async ({ where, orderBy, limit, offset }) => {
415
+ // Query your models database
416
+ const items = await db.models.findMany({
417
+ where: convertWhereToSQL(where),
418
+ orderBy: convertOrderByToSQL(orderBy),
419
+ take: limit,
420
+ skip: offset,
421
+ });
422
+
423
+ return { items, hasMore: items.length === limit };
424
+ },
425
+ },
426
+ {
427
+ description: "Get a model by URI",
428
+ handler: async ({ uri }) => {
429
+ const parsed = parseCollectionUri(uri);
430
+ const item = await db.models.findUnique({
431
+ where: { id: parsed.itemId },
432
+ });
433
+ return { item };
434
+ },
435
+ },
436
+ {
437
+ description: "Get streaming endpoint",
438
+ handler: async () => {
439
+ // Return your streaming endpoint URL
440
+ return { url: "https://api.example.com/v1/chat/completions" };
441
+ },
442
+ },
443
+ ]);
444
+ ```
445
+
446
+ ### Where Expression Structure
447
+
448
+ The `where` parameter supports TanStack DB predicate push-down patterns:
449
+
450
+ ```typescript
451
+ // Simple comparison
452
+ {
453
+ field: ["category"],
454
+ operator: "eq",
455
+ value: "electronics"
456
+ }
457
+
458
+ // Logical operators
459
+ {
460
+ operator: "and",
461
+ conditions: [
462
+ { field: ["category"], operator: "eq", value: "electronics" },
463
+ { field: ["price"], operator: "lt", value: 100 }
464
+ ]
465
+ }
466
+ ```
467
+
468
+ **Supported Operators:**
469
+ - Comparison: `eq`, `gt`, `gte`, `lt`, `lte`, `in`, `like`, `contains`
470
+ - Logical: `and`, `or`, `not`
471
+
472
+ ### Order By Expression Structure
473
+
474
+ The `orderBy` parameter supports multi-field sorting:
475
+
476
+ ```typescript
477
+ [
478
+ {
479
+ field: ["price"],
480
+ direction: "asc",
481
+ nulls: "last" // optional: "first" | "last"
482
+ },
483
+ {
484
+ field: ["created_at"],
485
+ direction: "desc"
486
+ }
487
+ ]
488
+ ```
489
+
490
+ ### Collection URI Helpers
491
+
492
+ The package provides utility functions for working with collection URIs:
493
+
494
+ ```typescript
495
+ import {
496
+ validateCollectionUri,
497
+ parseCollectionUri,
498
+ constructCollectionUri,
499
+ } from "@decocms/bindings/well-known/collections";
500
+
501
+ // Validate a URI
502
+ const isValid = validateCollectionUri("rsc://conn-123/users/user-456");
503
+ // true
504
+
505
+ // Parse a URI into components
506
+ const parsed = parseCollectionUri("rsc://conn-123/users/user-456");
507
+ // { connectionId: "conn-123", collectionName: "users", itemId: "user-456" }
508
+
509
+ // Construct a URI from components
510
+ const uri = constructCollectionUri("conn-123", "users", "user-456");
511
+ // "rsc://conn-123/users/user-456"
512
+ ```
513
+
514
+ ### TanStack DB Integration
515
+
516
+ Collection bindings are designed to work with TanStack DB's query-collection. The `where` and `orderBy` expressions are compatible with TanStack DB's `LoadSubsetOptions`, allowing for efficient predicate push-down to your backend.
517
+
518
+ ### MCP Implementation Example
519
+
520
+ Here's how you would implement a collection binding in an MCP server:
521
+
522
+ ```typescript
523
+ import { z } from "zod";
524
+ import { createCollectionBindings } from "@decocms/bindings/well-known/collections";
525
+ import { parseCollectionUri, constructCollectionUri } from "@decocms/bindings/well-known/collections";
526
+ import { impl } from "@decocms/sdk/mcp/bindings/binder";
527
+
528
+ const TodoSchema = z.object({
529
+ uri: z.string(),
530
+ created_at: z.string().datetime(),
531
+ updated_at: z.string().datetime(),
532
+ created_by: z.string().optional(),
533
+ updated_by: z.string().optional(),
534
+ title: z.string(),
535
+ completed: z.boolean(),
536
+ userId: z.number(),
537
+ });
538
+
539
+ const TODO_COLLECTION_BINDING = createCollectionBindings("todos", TodoSchema);
540
+
541
+ // Implement the tools
542
+ const todoTools = impl(TODO_COLLECTION_BINDING, [
543
+ {
544
+ description: "List todos with filtering and sorting",
545
+ handler: async ({ where, orderBy, limit, offset }) => {
546
+ // Convert where/orderBy to SQL and query database
547
+ const items = await db.todos.findMany({
548
+ where: convertWhereToSQL(where),
549
+ orderBy: convertOrderByToSQL(orderBy),
550
+ take: limit,
551
+ skip: offset,
552
+ });
553
+
554
+ return { items, hasMore: items.length === limit };
555
+ },
556
+ },
557
+ {
558
+ description: "Get a todo by URI",
559
+ handler: async ({ uri }) => {
560
+ const parsed = parseCollectionUri(uri);
561
+ const item = await db.todos.findUnique({
562
+ where: { id: parsed.itemId },
563
+ });
564
+ return { item };
565
+ },
566
+ },
567
+ {
568
+ description: "Create a new todo",
569
+ handler: async ({ data }) => {
570
+ const item = await db.todos.create({ data });
571
+ const uri = constructCollectionUri(
572
+ connectionId,
573
+ "todos",
574
+ item.id
575
+ );
576
+ return { item: { ...item, uri } };
577
+ },
578
+ },
579
+ {
580
+ description: "Update a todo",
581
+ handler: async ({ uri, data }) => {
582
+ const parsed = parseCollectionUri(uri);
583
+ const item = await db.todos.update({
584
+ where: { id: parsed.itemId },
585
+ data,
586
+ });
587
+ return { item };
588
+ },
589
+ },
590
+ {
591
+ description: "Delete a todo",
592
+ handler: async ({ uri }) => {
593
+ const parsed = parseCollectionUri(uri);
594
+ await db.todos.delete({ where: { id: parsed.itemId } });
595
+ return { success: true, uri };
596
+ },
597
+ },
598
+ ]);
599
+ ```
600
+
601
+ ### Type Safety
602
+
603
+ TypeScript can infer types from your collection binding definitions:
604
+
605
+ ```typescript
606
+ import type {
607
+ CollectionBinding,
608
+ CollectionTools,
609
+ CollectionListInput,
610
+ CollectionGetInput,
611
+ CollectionDeleteInput,
612
+ } from "@decocms/bindings/well-known/collections";
613
+
614
+ // Extract the binding type
615
+ type TodoBinding = typeof TODO_COLLECTION_BINDING;
616
+
617
+ // Extract tool names
618
+ type TodoTools = CollectionTools<typeof TodoSchema>;
619
+ // "DECO_COLLECTION_TODOS_LIST" | "DECO_COLLECTION_TODOS_GET" | ...
620
+
621
+ // Get input types
622
+ type ListInput = CollectionListInput;
623
+ type GetInput = CollectionGetInput;
624
+ type DeleteInput = CollectionDeleteInput;
625
+ ```
626
+
627
+ ## Development
628
+
629
+ ### Setup
630
+
631
+ ```bash
632
+ # Install dependencies
633
+ bun install
634
+
635
+ # Build the package
636
+ bun run build
637
+
638
+ # Run tests
639
+ bun run test
640
+
641
+ # Watch mode for tests
642
+ bun run test:watch
643
+ ```
644
+
645
+ ### Building
646
+
647
+ The package uses [tsup](https://tsup.egoist.dev/) for building:
648
+
649
+ ```bash
650
+ bun run build
651
+ ```
652
+
653
+ This will generate ESM output in the `dist/` directory with TypeScript declarations.
654
+
655
+ ### Testing
656
+
657
+ Tests are written with [Vitest](https://vitest.dev/):
658
+
659
+ ```bash
660
+ # Run tests once
661
+ bun run test
662
+
663
+ # Watch mode
664
+ bun run test:watch
665
+ ```
666
+
667
+ ## Publishing
668
+
669
+ The package is automatically published to npm when a tag matching `bindings-v*` is pushed:
670
+
671
+ ```bash
672
+ # Update version in package.json, then:
673
+ git tag bindings-v0.1.0
674
+ git push origin bindings-v0.1.0
675
+ ```
676
+
677
+ ## License
678
+
679
+ See the root LICENSE.md file in the repository.
680
+