@ecodrix/erix-api 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ECODrIx Team
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.
package/README.md ADDED
@@ -0,0 +1,643 @@
1
+ # @ecodrix/erix-api
2
+
3
+ <div align="center">
4
+
5
+ [![NPM Version](https://img.shields.io/npm/v/@ecodrix/erix-api.svg?style=for-the-badge)](https://www.npmjs.com/package/@ecodrix/erix-api)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=for-the-badge&logo=typescript)](https://www.typescriptlang.org/)
8
+ [![OpenAPI](https://img.shields.io/badge/OpenAPI-3.0-green?style=for-the-badge&logo=openapiinitiative)](./schema/openapi.yaml)
9
+
10
+ **The official, isomorphic SDK for the [ECODrIx](https://ecodrix.com) platform.**
11
+
12
+ Manage WhatsApp conversations, CRM leads, file storage, and Google Meet appointments — all from a single, type-safe library.
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## Table of Contents
19
+
20
+ - [Installation](#installation)
21
+ - [Quick Start](#quick-start)
22
+ - [Configuration](#configuration)
23
+ - [Resources](#resources)
24
+ - [WhatsApp](#whatsapp)
25
+ - [CRM — Leads](#crm--leads)
26
+ - [Meetings](#meetings)
27
+ - [Media (Storage)](#media-storage)
28
+ - [Email](#email)
29
+ - [Events & Workflows](#events--workflows)
30
+ - [Notifications & Logs](#notifications--logs)
31
+ - [Enterprise Capabilities](#enterprise-capabilities)
32
+ - [Auto-Paginating Iterators](#auto-paginating-iterators)
33
+ - [Bulk Data Chunking](#bulk-data-chunking)
34
+ - [Idempotency Keys](#idempotency-keys)
35
+ - [Webhook Signature Verification](#webhook-signature-verification)
36
+ - [Raw Execution Engine](#raw-execution-engine)
37
+ - [Real-time Events](#real-time-events)
38
+ - [Error Handling](#error-handling)
39
+ - [Browser / CDN Usage](#browser--cdn-usage)
40
+
41
+ ---
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ # pnpm (recommended)
47
+ pnpm add @ecodrix/erix-api
48
+
49
+ # npm
50
+ npm install @ecodrix/erix-api
51
+
52
+ # yarn
53
+ yarn add @ecodrix/erix-api
54
+ ```
55
+
56
+ > **Requires**: Node.js >= 18
57
+
58
+ ---
59
+
60
+ ## Quick Start
61
+
62
+ ```typescript
63
+ import { Ecodrix } from "@ecodrix/erix-api";
64
+
65
+ const ecod = new Ecodrix({
66
+ apiKey: "your_api_key",
67
+ clientCode: "YOUR_CLIENT_CODE", // Your tenant ID
68
+ });
69
+
70
+ // Send a WhatsApp message
71
+ await ecod.whatsapp.messages.send({
72
+ to: "+919876543210",
73
+ text: "Hello from ECODrIx!",
74
+ });
75
+
76
+ // Create a CRM lead
77
+ const lead = await ecod.crm.leads.create({
78
+ firstName: "Jhon",
79
+ phone: "+919876543210",
80
+ source: "website",
81
+ });
82
+
83
+ console.log(lead.data.id);
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Configuration
89
+
90
+ Pass an options object to `new Ecodrix(options)`:
91
+
92
+ | Option | Type | Required | Default | Description |
93
+ | ------------ | -------- | ----------- | ------------------------- | ---------------------------------------------- |
94
+ | `apiKey` | `string` | ✅ Yes | — | Your ECOD Platform API key |
95
+ | `clientCode` | `string` | Recommended | — | Your tenant ID (scopes all requests) |
96
+ | `baseUrl` | `string` | No | `https://api.ecodrix.com` | Override the API base URL (e.g. for local dev) |
97
+ | `socketUrl` | `string` | No | Same as `baseUrl` | Override the Socket.io server URL |
98
+
99
+ ```typescript
100
+ const ecod = new Ecodrix({
101
+ apiKey: process.env.ECOD_API_KEY!,
102
+ clientCode: process.env.ECOD_CLIENT_CODE,
103
+ baseUrl: "http://localhost:4000", // For local development
104
+ });
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Resources
110
+
111
+ Every resource follows the same predictable CRUD interface:
112
+
113
+ | Method | Description |
114
+ | --------------------- | ---------------------------------- |
115
+ | `.create(params)` | Create a new record |
116
+ | `.list(params?)` | List records with optional filters |
117
+ | `.retrieve(id)` | Get a single record by ID |
118
+ | `.update(id, params)` | Update a record |
119
+ | `.delete(id)` | Delete or cancel a record |
120
+
121
+ ---
122
+
123
+ ### WhatsApp
124
+
125
+ Access via `ecod.whatsapp`.
126
+
127
+ #### `ecod.whatsapp.messages`
128
+
129
+ **Send a text message**
130
+
131
+ ```typescript
132
+ await ecod.whatsapp.messages.send({
133
+ to: "+919876543210",
134
+ text: "Your appointment is confirmed!",
135
+ });
136
+ ```
137
+
138
+ **Send a media message (image, video, document, audio)**
139
+
140
+ ```typescript
141
+ await ecod.whatsapp.messages.send({
142
+ to: "+919876543210",
143
+ mediaUrl: "https://cdn.ecodrix.com/invoice.pdf",
144
+ mediaType: "document",
145
+ filename: "invoice.pdf",
146
+ });
147
+ ```
148
+
149
+ **Send a pre-approved template message**
150
+
151
+ ```typescript
152
+ await ecod.whatsapp.messages.sendTemplate({
153
+ to: "+919876543210",
154
+ templateName: "appointment_reminder",
155
+ language: "en_US",
156
+ variables: ["Dhanesh", "Tomorrow 10AM"],
157
+ });
158
+ ```
159
+
160
+ **Send a Meta-approved direct template (Bypass queues)**
161
+
162
+ Use this specifically to dispatch high-priority utility templates containing dynamic variable maps.
163
+
164
+ ```typescript
165
+ const { messageId } = await ecod.whatsapp.sendTemplate({
166
+ phone: "+919876543210",
167
+ templateName: "payment_confirmation",
168
+ variables: {
169
+ name: "Dhanesh",
170
+ amount: "₹1,500"
171
+ }
172
+ });
173
+ ```
174
+
175
+ **Mark a conversation as read**
176
+
177
+ ```typescript
178
+ await ecod.whatsapp.messages.markRead("conversation_id");
179
+ ```
180
+
181
+ #### `ecod.whatsapp.conversations`
182
+
183
+ ```typescript
184
+ // List conversations (cursor-based pagination)
185
+ const { data } = await ecod.whatsapp.conversations.list({ limit: 20 });
186
+
187
+ // Get a specific conversation
188
+ const conv = await ecod.whatsapp.conversations.retrieve("conversation_id");
189
+
190
+ // Archive a conversation
191
+ await ecod.whatsapp.conversations.delete("conversation_id");
192
+ ```
193
+
194
+ ---
195
+
196
+ ### CRM — Leads
197
+
198
+ Access via `ecod.crm.leads`.
199
+
200
+ **Create a Lead**
201
+
202
+ ```typescript
203
+ const { data: lead } = await ecod.crm.leads.create({
204
+ firstName: "Dhanesh",
205
+ lastName: "Kumar",
206
+ phone: "+919876543210",
207
+ email: "dhanesh@example.com",
208
+ source: "website", // 'website' | 'whatsapp' | 'direct' | ...
209
+ metadata: {
210
+ utmSource: "google",
211
+ },
212
+ });
213
+ ```
214
+
215
+ **List Leads (with filters)**
216
+
217
+ ```typescript
218
+ const { data: leads } = await ecod.crm.leads.list({
219
+ status: "new", // filter by status
220
+ source: "whatsapp",
221
+ page: 1,
222
+ limit: 25,
223
+ });
224
+ ```
225
+
226
+ *(See [Enterprise Capabilities](#enterprise-capabilities) for efficient auto-pagination or bulk-creation tricks like `listAutoPaging` and `createMany`).*
227
+
228
+ **Retrieve a Lead by ID**
229
+
230
+ ```typescript
231
+ const { data: lead } = await ecod.crm.leads.retrieve("lead_id");
232
+ ```
233
+
234
+ **Update a Lead**
235
+
236
+ ```typescript
237
+ await ecod.crm.leads.update("lead_id", {
238
+ email: "new@email.com",
239
+ metadata: { score: 95 },
240
+ });
241
+ ```
242
+
243
+ **Delete / Archive a Lead**
244
+
245
+ ```typescript
246
+ await ecod.crm.leads.delete("lead_id");
247
+ ```
248
+
249
+ ---
250
+
251
+ ### Meetings
252
+
253
+ Access via `ecod.meet`. Backed by **Google Meet**.
254
+
255
+ **Schedule a Meeting**
256
+
257
+ ```typescript
258
+ const { data: meeting } = await ecod.meet.create({
259
+ leadId: "lead_id",
260
+ participantName: "Dhanesh Kumar",
261
+ participantPhone: "+919876543210",
262
+ startTime: "2026-04-10T10:00:00.000Z",
263
+ endTime: "2026-04-10T10:30:00.000Z",
264
+ });
265
+
266
+ console.log(meeting.meetLink); // → https://meet.google.com/abc-defg-hij
267
+ ```
268
+
269
+ **List Meetings**
270
+
271
+ ```typescript
272
+ const { data: meetings } = await ecod.meet.list({ status: "scheduled" });
273
+ ```
274
+
275
+ **Retrieve a Meeting**
276
+
277
+ ```typescript
278
+ const { data } = await ecod.meet.retrieve("meeting_id");
279
+ ```
280
+
281
+ **Reschedule a Meeting**
282
+
283
+ ```typescript
284
+ await ecod.meet.update("meeting_id", {
285
+ startTime: "2026-04-11T11:00:00.000Z",
286
+ endTime: "2026-04-11T11:30:00.000Z",
287
+ });
288
+ ```
289
+
290
+ **Cancel a Meeting**
291
+
292
+ ```typescript
293
+ await ecod.meet.delete("meeting_id");
294
+ ```
295
+
296
+ ---
297
+
298
+ ### Storage
299
+
300
+ Access via `ecod.storage`. Powered by **Cloudflare R2**.
301
+
302
+ **Upload a File (Elite Helper)**
303
+
304
+ This single call handles the entire R2 presigned-URL orchestration for you:
305
+
306
+ 1. Requests a presigned PUT URL from the backend.
307
+ 2. Uploads the file directly to R2 (no proxy overhead).
308
+ 3. Confirms the upload with the backend.
309
+
310
+ ```typescript
311
+ // Node.js: from a Buffer
312
+ import { readFileSync } from "fs";
313
+
314
+ const fileBuffer = readFileSync("./contract.pdf");
315
+ const { data } = await ecod.storage.upload(fileBuffer, {
316
+ folder: "customer_documents",
317
+ filename: "contract.pdf",
318
+ contentType: "application/pdf",
319
+ });
320
+
321
+ console.log(data.url); // → https://cdn.ecodrix.com/customer_documents/contract.pdf
322
+
323
+ // Browser: from an <input> file
324
+ const file = document.getElementById("file-input").files[0];
325
+ const { data } = await ecod.storage.upload(file, {
326
+ folder: "avatars",
327
+ filename: file.name,
328
+ contentType: file.type,
329
+ });
330
+ filename: "dhanesh.png",
331
+ contentType: "image/png"
332
+ });
333
+
334
+ console.log(data.url);
335
+ // -> https://cdn.ecodrix.com/avatars/dhanesh.png
336
+ ```
337
+
338
+ **Get a Presigned URL for Private Assets**
339
+
340
+ ```javascript
341
+ const { data } = await ecod.media.getDownloadUrl("confidential/contract.pdf");
342
+ ```
343
+
344
+ **Monitor Quota & Usage**
345
+
346
+ ```javascript
347
+ const { data: usage } = await ecod.media.getUsage();
348
+ console.log(`${usage.usedMB} MB used of ${usage.limitMB} MB`);
349
+ ```
350
+
351
+ ---
352
+
353
+ ### Email
354
+
355
+ Access via `ecod.email`.
356
+
357
+ **Send an Email Campaign**
358
+
359
+ Dispatch an HTML email campaign to a given array of recipients. Powered by Ecodrix or AWS SES depending on tenant configuration.
360
+
361
+ ```typescript
362
+ const { success } = await ecod.email.sendEmailCampaign({
363
+ subject: "Summer Subscription Discount!",
364
+ recipients: ["user@example.com", "lead@example.com"],
365
+ html: "<h1>Get 20% off all plans today!</h1>",
366
+ });
367
+ ```
368
+
369
+ **Send a Test Email**
370
+
371
+ Send a system verification email to validate SMTP configuration for your tenant.
372
+
373
+ ```typescript
374
+ const { success } = await ecod.email.sendTestEmail("admin@example.com");
375
+ ```
376
+
377
+ ---
378
+
379
+ ### Events & Workflows
380
+
381
+ Access via `ecod.events`. Programmatically integrate the CRM's automation engine with your external apps.
382
+
383
+ **Retrieve Global Triggers**
384
+
385
+ Fetch all active automation triggers belonging to the current tenant.
386
+
387
+ ```typescript
388
+ const { data: triggers } = await ecod.events.list();
389
+ ```
390
+
391
+ **Register a Custom Event Definition**
392
+
393
+ Register a new entry point that you want to show up in the CRM Automation Builder.
394
+
395
+ ```typescript
396
+ await ecod.events.assign({
397
+ name: "cart_abandoned",
398
+ displayName: "Shopping Cart Abandoned",
399
+ pipelineId: "pipeline_123", /* Auto map leads to this pipeline */
400
+ });
401
+ ```
402
+
403
+ **Emit a Payload / Trigger Workflow**
404
+
405
+ Inject generic data into the `EventBus` to fire customized automation rules.
406
+
407
+ ```typescript
408
+ await ecod.events.trigger({
409
+ trigger: "cart_abandoned",
410
+ phone: "+919876543210", // Primary matching key
411
+ variables: { items: "T-Shirt, Mug", total: "₹850" },
412
+ createLeadIfMissing: true, // Upsert lead if it's a new customer
413
+ });
414
+ ```
415
+
416
+ ---
417
+
418
+ ### Notifications & Logs
419
+
420
+ Access via `ecod.notifications`. View automation and webhook audit logs.
421
+
422
+ **List Automation Execution Logs**
423
+
424
+ ```typescript
425
+ const { data: logs } = await ecod.notifications.listLogs({
426
+ trigger: "lead_created",
427
+ status: "failed", // filter to only failed automations
428
+ startDate: "2026-04-01",
429
+ endDate: "2026-04-30",
430
+ limit: 50,
431
+ });
432
+ ```
433
+
434
+ **Retrieve a Specific Log**
435
+
436
+ ```typescript
437
+ const { data: log } = await ecod.notifications.retrieveLog("log_id");
438
+ ```
439
+
440
+ **Get Automation Stats Summary**
441
+
442
+ ```typescript
443
+ const { data: stats } = await ecod.notifications.getStats({
444
+ startDate: "2026-04-01",
445
+ endDate: "2026-04-30",
446
+ });
447
+ ```
448
+
449
+ **List Provider Callback Logs (Webhooks)**
450
+
451
+ ```typescript
452
+ const { data: callbacks } = await ecod.notifications.listCallbacks({
453
+ limit: 20,
454
+ });
455
+ ```
456
+
457
+ ---
458
+
459
+ ## Enterprise Capabilities
460
+
461
+ The SDK is equipped with state-of-the-art native patterns for robust execution, zero-downtime performance, and unparalleled developer experience. All network requests automatically utilize an exponential-backoff retry engine to gracefully handle temporary network errors.
462
+
463
+ ### Auto-Paginating Iterators
464
+ To rapidly ingest records without manually iterating pages, use standard Node.js `for await` loops. The SDK controls memory buffers and page fetching dynamically.
465
+
466
+ ```typescript
467
+ // Look how beautiful this developer experience is:
468
+ for await (const lead of ecod.crm.leads.listAutoPaging({ status: "won" })) {
469
+ await syncToMyDatabase(lead);
470
+ }
471
+ ```
472
+
473
+ ### Bulk Data Chunking
474
+ Need to insert 5,000 leads at once but worried about network congestion or rate limits? `createMany` automatically chunks arrays into concurrent streams.
475
+
476
+ ```typescript
477
+ // The users array chunked into safe parallel batched requests
478
+ const results = await ecod.crm.leads.createMany(massiveArrayOfLeads, 100);
479
+ console.log(`Successfully ingested ${results.length} leads.`);
480
+ ```
481
+
482
+ ### Idempotency Keys
483
+ Because the SDK provides an automatic network retry engine (`axios-retry`), temporary blips could duplicate events. Passing an `idempotencyKey` strictly tells the backend: "Only execute this once, even if I accidentally retry the same payload."
484
+
485
+ ```typescript
486
+ await ecod.email.sendEmailCampaign(
487
+ { subject: "Promo", recipients: ["user@example.com"] },
488
+ { idempotencyKey: "promo_campaign_uuid_123_abc" }
489
+ );
490
+ ```
491
+
492
+ ### Webhook Signature Verification
493
+ Verify cryptographic webhook payloads issued by the ECODrIx platform reliably in Node environments, mitigating spoofing attacks seamlessly.
494
+
495
+ ```typescript
496
+ app.post("/api/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
497
+ try {
498
+ const event = await ecod.webhooks.constructEvent(
499
+ req.body.toString(),
500
+ req.headers['x-ecodrix-signature'],
501
+ "whsec_your_secret_key"
502
+ );
503
+ console.log("Verified event received:", event);
504
+ } catch (err) {
505
+ return res.status(400).send(`Invalid signature: ${err.message}`);
506
+ }
507
+ });
508
+ ```
509
+
510
+ ### Raw Execution Engine
511
+ Need to execute an experimental, undocumented, or beta endpoint but still want full authentication mapping, global headers, and intelligent retries?
512
+
513
+ ```typescript
514
+ // Bypass strictly typed resources with full network resiliency
515
+ const customData = await ecod.request(
516
+ "POST",
517
+ "/api/saas/beta-feature-route",
518
+ { experimentalFlag: true }
519
+ );
520
+ ```
521
+
522
+ ---
523
+
524
+ ## Real-time Events
525
+
526
+ The SDK maintains a persistent **Socket.io** connection. Subscribe to live platform events using `ecod.on(event, handler)`.
527
+
528
+ ```typescript
529
+ const ecod = new Ecodrix({ apiKey: "...", clientCode: "..." });
530
+
531
+ // New incoming WhatsApp message
532
+ ecod.on("whatsapp.message_received", (data) => {
533
+ console.log(`New message from ${data.from}: ${data.body}`);
534
+ });
535
+
536
+ // Lead stage changed in CRM
537
+ ecod.on("crm.lead_updated", (data) => {
538
+ console.log(`Lead ${data.leadId} moved to stage: ${data.stageName}`);
539
+ });
540
+
541
+ // Automation or event failed — for alert pipelines
542
+ ecod.on("automation.failed", (data) => {
543
+ console.error(`Automation failed: ${data.trigger} — ${data.reason}`);
544
+ });
545
+
546
+ // Storage upload completed
547
+ ecod.on("storage.upload_confirmed", (data) => {
548
+ console.log(`File available at: ${data.url}`);
549
+ });
550
+
551
+ // Disconnect when done (e.g. server shutdown)
552
+ ecod.disconnect();
553
+ ```
554
+
555
+ ### Standard Event Names
556
+
557
+ | Event | Payload | Description |
558
+ | --------------------------- | -------------------------------- | --------------------------- |
559
+ | `whatsapp.message_received` | `{ from, body, conversationId }` | Inbound WhatsApp message |
560
+ | `whatsapp.message_sent` | `{ to, messageId }` | Outbound message delivered |
561
+ | `crm.lead_created` | `{ leadId, phone }` | New CRM lead created |
562
+ | `crm.lead_updated` | `{ leadId, stageName }` | Lead stage or field changed |
563
+ | `meet.scheduled` | `{ meetingId, meetLink }` | New Google Meet booked |
564
+ | `storage.upload_confirmed` | `{ key, url, sizeBytes }` | File upload confirmed |
565
+ | `automation.failed` | `{ trigger, reason, leadId }` | Automation execution failed |
566
+
567
+ ---
568
+
569
+ ## Error Handling
570
+
571
+ All methods throw typed errors you can catch and inspect:
572
+
573
+ ```typescript
574
+ import {
575
+ Ecodrix,
576
+ APIError,
577
+ AuthenticationError,
578
+ RateLimitError,
579
+ } from "@ecodrix/erix-api";
580
+
581
+ try {
582
+ const { data } = await ecod.crm.leads.retrieve("non_existent_id");
583
+ } catch (err) {
584
+ if (err instanceof AuthenticationError) {
585
+ // 401 — invalid API key or client code
586
+ console.error("Check your credentials.");
587
+ } else if (err instanceof RateLimitError) {
588
+ // 429 — slow down requests
589
+ console.warn("Rate limit hit. Retrying after delay...");
590
+ } else if (err instanceof APIError) {
591
+ // Other HTTP errors from the API
592
+ console.error(`API Error [${err.status}]: ${err.message}`);
593
+ } else {
594
+ throw err; // re-throw unknown errors
595
+ }
596
+ }
597
+ ```
598
+
599
+ ### Error Classes
600
+
601
+ | Class | Status | Code | Description |
602
+ | --------------------- | ------ | --------------------- | ------------------------------------------ |
603
+ | `EcodrixError` | — | — | Base error class |
604
+ | `APIError` | varies | varies | Generic API error with `status` and `code` |
605
+ | `AuthenticationError` | 401 | `AUTH_FAILED` | Invalid API key or client code |
606
+ | `RateLimitError` | 429 | `RATE_LIMIT_EXCEEDED` | Too many requests |
607
+
608
+ ---
609
+
610
+ ## Browser / CDN Usage
611
+
612
+ For usage without a bundler (plain HTML, marketing pages):
613
+
614
+ ```html
615
+ <!-- Via CDN (jsDelivr) -->
616
+ <script src="https://cdn.jsdelivr.net/npm/@ecodrix/erix-api/dist/ts/browser/index.global.js"></script>
617
+ <script>
618
+ const ecod = new Ecodrix.Ecodrix({
619
+ apiKey: "your_api_key",
620
+ clientCode: "YOUR_CLIENT_CODE",
621
+ });
622
+
623
+ ecod.whatsapp.messages.send({
624
+ to: "+919876543210",
625
+ text: "Hello from the browser!",
626
+ });
627
+ </script>
628
+ ```
629
+
630
+ ---
631
+
632
+ ## Contributing
633
+
634
+ 1. Fork the repository.
635
+ 2. Create a feature branch: `git checkout -b feat/my-feature`
636
+ 3. Make changes, then run `pnpm check` to validate.
637
+ 4. Submit a Pull Request.
638
+
639
+ ---
640
+
641
+ ## License
642
+
643
+ [MIT](LICENSE) © 2026 [ECODrIx Team](https://ecodrix.com)